@wirecat/cli-core 0.407.0 → 0.409.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/background/schedules.d.ts +1 -1
  3. package/dist/background/schedules.js +3 -1
  4. package/dist/cli/bot/admins.js +5 -3
  5. package/dist/cli/bot/command.js +21 -11
  6. package/dist/cli/bot/list-queries.d.ts +27 -0
  7. package/dist/cli/bot/list-queries.js +83 -0
  8. package/dist/cli/bot/messages.js +14 -8
  9. package/dist/cli/bot/registry.d.ts +1 -1
  10. package/dist/cli/bot/registry.js +4 -2
  11. package/dist/cli/bot/setup.js +8 -4
  12. package/dist/cli/command-availability.d.ts +14 -0
  13. package/dist/cli/command-availability.js +25 -0
  14. package/dist/cli/commands-command.d.ts +1 -1
  15. package/dist/cli/commands-command.js +17 -4
  16. package/dist/cli/context.d.ts +1 -0
  17. package/dist/cli/effects.d.ts +1 -1
  18. package/dist/cli/effects.js +1 -1
  19. package/dist/cli/help/command-help.js +18 -1
  20. package/dist/cli/help/commands-text.d.ts +1 -1
  21. package/dist/cli/help/commands-text.js +3 -2
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.js +1 -0
  24. package/dist/cli/messenger/list-result-queries.d.ts +9 -0
  25. package/dist/cli/messenger/list-result-queries.js +30 -0
  26. package/dist/cli/messenger/recipients-query.d.ts +1 -1
  27. package/dist/cli/messenger/recipients-query.js +2 -2
  28. package/dist/cli/messenger/records-command.js +14 -4
  29. package/dist/cli/messenger/schedule-command.js +6 -3
  30. package/dist/cli/messenger/sends-query.d.ts +1 -1
  31. package/dist/cli/messenger/sends-query.js +2 -2
  32. package/dist/cli/messenger/topics-command.js +10 -6
  33. package/dist/cli/program.d.ts +2 -1
  34. package/dist/cli/program.js +24 -1
  35. package/dist/entities/index.d.ts +84 -1
  36. package/dist/entities/index.js +11 -1
  37. package/dist/entities/list-results.d.ts +83 -0
  38. package/dist/entities/list-results.js +69 -0
  39. package/dist/mcp/agent.d.ts +11 -11
  40. package/dist/mcp/agent.js +13 -27
  41. package/dist/mcp/bot/instructions.js +2 -1
  42. package/dist/mcp/bot/server.d.ts +2 -2
  43. package/dist/mcp/bot/server.js +29 -7
  44. package/dist/mcp/index.d.ts +1 -1
  45. package/dist/mcp/instructions.js +2 -1
  46. package/dist/mcp/server.js +33 -7
  47. package/dist/mcp/surface.d.ts +3 -1
  48. package/dist/mcp/surface.js +8 -2
  49. package/dist/meetings/mcp/index.d.ts +15 -15
  50. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ 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
+ ## 0.409.0 — 24.10.2026
9
+
10
+ ### Added
11
+
12
+ - **Shared lists declare typed queries, including bots, schedules, forum topics, calls and stickers.** Stored bot chats, recipients, send history,
13
+ profiles and schedules filter at their native readers; forum title terms go to the provider. Message predicates
14
+ require `--offline` and run in SQLite before the limit, reporting interpreted time bounds. Provider lists that
15
+ accept no filters refuse terms before connecting, instead of filtering an incomplete response. Existing empty
16
+ queries keep their previous behavior. Shared list-result contracts include the template schema for provider CLIs.
17
+
18
+ ## 0.408.0 — 24.10.2026
19
+
20
+ ### Fixed
21
+
22
+ - **MCP discovery retains permission-denied commands.** Full listings and searches return each command's
23
+ current `availability`, including a reason and permission key when blocked. With a named profile,
24
+ CLI help and command discovery also show current availability without hiding commands. Read-only profiles and process
25
+ locks keep writing commands discoverable. Calls still recheck permissions and refuse before execution;
26
+ seeing a command does not grant access. Bot MCP also lists bot-scoped `config show|set|unset` behind the
27
+ existing authority rules, without exposing options that could select personal-account settings. Consumers
28
+ that pin MCP catalogues should refresh them and use availability rather than absence to determine access.
29
+
8
30
  ## 0.407.0 — 24.10.2026
9
31
 
10
32
  ### Added
@@ -75,7 +75,7 @@ export declare const scheduledRuns: (state: string) => {
75
75
  job: string;
76
76
  last: LastRun | undefined;
77
77
  }[];
78
- export declare const listSchedules: (state: string) => Schedule[];
78
+ export declare const listSchedules: (state: string, filter?: Partial<Pick<Schedule, "name" | "kind">>) => Schedule[];
79
79
  export declare const describeWhen: (when: When) => string;
80
80
  /** How one operating system starts `argv` at the schedule's times. Every timer runs the same thing: `<cli> schedule exec <file>`. */
81
81
  export interface Timer {
@@ -98,11 +98,13 @@ export const scheduledRuns = (state) => {
98
98
  return [];
99
99
  }
100
100
  };
101
- export const listSchedules = (state) => existsSync(schedulesDir(state))
101
+ export const listSchedules = (state, filter = {}) => existsSync(schedulesDir(state))
102
102
  ? readdirSync(schedulesDir(state))
103
103
  .map((name) => scheduleFile(state, name))
104
104
  .filter((file) => existsSync(file))
105
105
  .map(readSchedule)
106
+ .filter((schedule) => (filter.name === undefined || schedule.name === filter.name) &&
107
+ (filter.kind === undefined || schedule.kind === filter.kind))
106
108
  .sort((a, b) => a.name.localeCompare(b.name))
107
109
  : [];
108
110
  export const describeWhen = (when) => "every" in when
@@ -6,10 +6,12 @@ import { guardedWrite } from "../../sends/guarded.js";
6
6
  import { newOperationId } from "../../sends/send-id.js";
7
7
  import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
8
8
  import { renderAnswer, snakeKeys } from "../envelope.js";
9
+ import { listQuery, typedQuery, withQuery } from "../list-query.js";
9
10
  import { rightsOf } from "../messenger/admin-commands.js";
10
11
  import { renderItem, wholeList } from "../messenger/chat-answers.js";
11
12
  import { botContext, online } from "./context.js";
12
13
  import { botExamples } from "./examples.js";
14
+ import { BOT_ADMINS_QUERY } from "./list-queries.js";
13
15
  import { botCan } from "./messages.js";
14
16
  const CHAT = "a chat id, or the title of a chat this bot has seen";
15
17
  const PERSON = "the person's user id";
@@ -24,10 +26,10 @@ const personOf = (typed) => {
24
26
  export const botAdminsCommand = (bot) => {
25
27
  const offered = bot.adminRights ?? ADMIN_RIGHTS;
26
28
  const command = new Command("admins").description("the admins of a chat the bot is an admin in");
27
- annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats admins list Team") })
29
+ withQuery(annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats admins list Team") })
28
30
  .description("the chat's admins and what each may do")
29
- .argument("<chat>", CHAT)
30
- .action(async function (chat) {
31
+ .argument("<chat>", CHAT), BOT_ADMINS_QUERY).action(async function (chat, words) {
32
+ listQuery(typedQuery(words), BOT_ADMINS_QUERY, "bot chats admins list");
31
33
  const context = online(botContext(this, bot), this);
32
34
  const ref = context.chatRef(chat);
33
35
  await context.run(async (events) => {
@@ -6,10 +6,14 @@ import { guardedWrite } from "../../sends/guarded.js";
6
6
  import { newOperationId } from "../../sends/send-id.js";
7
7
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_READ, REMOTE_WRITE } from "../effects.js";
8
8
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
9
+ import { withQuery } from "../list-query.js";
9
10
  import { renderItem, wholeList } from "../messenger/chat-answers.js";
11
+ import { RECIPIENTS_QUERY, recipientFilter } from "../messenger/recipients-query.js";
12
+ import { SENDS_QUERY, sendFilter } from "../messenger/sends-query.js";
10
13
  import { botAdminsCommand, botMembersCommand } from "./admins.js";
11
14
  import { botContext, online } from "./context.js";
12
15
  import { botExamples } from "./examples.js";
16
+ import { BOT_CHATS_QUERY, BOT_PROFILES_QUERY, botChatFilter, botProfileName } from "./list-queries.js";
13
17
  import { botMcpCommand } from "./mcp.js";
14
18
  import { botCan, botIdOf, botMessagesCommand } from "./messages.js";
15
19
  import { botModerateCommand, botRulesCommand } from "./moderation.js";
@@ -114,11 +118,12 @@ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ, e
114
118
  }
115
119
  });
116
120
  });
117
- const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "list", "list --check") })
121
+ const listCommand = (bot, tokenVariable) => withQuery(annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "list", "list --check") }), BOT_PROFILES_QUERY)
118
122
  .description(`every name on this machine that has a bot token; --check asks ${bot.name ?? "the messenger"} which bot each is`)
119
123
  .summary("every bot profile on this machine; --check asks which bot each is")
120
124
  .option("--check", "ask the messenger who each bot is, with its token")
121
- .action(async function () {
125
+ .action(async function (words) {
126
+ const wanted = botProfileName(words);
122
127
  const context = botContext(this, bot);
123
128
  const check = this.opts().check === true;
124
129
  if (check)
@@ -132,6 +137,8 @@ const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effe
132
137
  }
133
138
  const rows = [];
134
139
  for (const name of names) {
140
+ if (wanted !== undefined && name !== wanted)
141
+ continue;
135
142
  const tokens = bot.tokenStore?.(this, name) ??
136
143
  new BotTokenStore({
137
144
  app: bot.app,
@@ -161,11 +168,12 @@ const recipientsCommand = (bot) => {
161
168
  const command = new Command("recipients")
162
169
  .description("the chats this bot may write to; with no list, every chat — `clear` removes the list")
163
170
  .summary("the chats this bot may write to; with no list, every chat");
164
- annotate(command.command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "recipients list") })
171
+ withQuery(annotate(command.command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "recipients list") }), RECIPIENTS_QUERY)
165
172
  .description("the chats on the list, or nothing when there is no list")
166
- .action(async function () {
173
+ .action(async function (words) {
174
+ const filter = recipientFilter(words, "bot recipients list");
167
175
  const context = botContext(this, bot);
168
- await context.run(async () => renderAnswer(this, context, wholeList("bot recipients list", (context.recipients().read() ?? []).map(snakeKeys))));
176
+ await context.run(async () => renderAnswer(this, context, wholeList("bot recipients list", context.recipients().list(filter).items.map(snakeKeys))));
169
177
  });
170
178
  grantsAuthority(annotate(command.command("add"), {
171
179
  effects: LOCAL_WRITE,
@@ -203,21 +211,23 @@ const recipientsCommand = (bot) => {
203
211
  const sendsCommand = (bot) => new Command("sends")
204
212
  .description("what this bot sent, edited and deleted from this machine — ids and outcomes, never text")
205
213
  .summary("what this bot sent, edited and deleted from here — never the text")
206
- .addCommand(annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "sends list") })
214
+ .addCommand(withQuery(annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "sends list") }), SENDS_QUERY)
207
215
  .description("every send, edit and delete this bot made from this machine, with its outcome")
208
- .action(async function () {
216
+ .action(async function (words) {
217
+ const filter = sendFilter(words, "bot sends list");
209
218
  const context = botContext(this, bot);
210
- await context.run(async () => renderAnswer(this, context, wholeList("bot sends list", context.journal().entries().map(snakeKeys))));
219
+ await context.run(async () => renderAnswer(this, context, wholeList("bot sends list", context.journal().entries(filter).map(snakeKeys))));
211
220
  }));
212
221
  const chatsCommand = (bot) => {
213
222
  const command = new Command("chats")
214
223
  .description(`the chats this bot is in — ${bot.name ?? "the messenger"} gives a bot no list of them, so \`list\` shows the ones it has seen`)
215
224
  .summary("the chats this bot is in, and what it does there");
216
- annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats list") })
225
+ withQuery(annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats list") }), BOT_CHATS_QUERY)
217
226
  .description(`chats this bot has seen on this machine — not a complete list from ${bot.name ?? "the messenger"}`)
218
- .action(async function () {
227
+ .action(async function (words) {
228
+ const filter = botChatFilter(words);
219
229
  const context = botContext(this, bot);
220
- await context.run(async () => renderAnswer(this, context, wholeList("bot chats list", context.registry.list().map(snakeKeys))));
230
+ await context.run(async () => renderAnswer(this, context, wholeList("bot chats list", context.registry.list(filter).map(snakeKeys))));
221
231
  });
222
232
  annotate(command.command("show"), { effects: REMOTE_READ, examples: botExamples(bot, "chats show Team") })
223
233
  .description(`one chat from ${bot.name ?? "the messenger"}, and remember it`)
@@ -0,0 +1,27 @@
1
+ import { type QueryRegistry } from "@wirecat/cli-base/query";
2
+ import { type QueryWords } from "../list-query.js";
3
+ import type { SeenChat } from "./registry.js";
4
+ export declare const BOT_PROFILES_QUERY: QueryRegistry;
5
+ export declare const botProfileName: (words: QueryWords) => string | undefined;
6
+ export declare const BOT_CHATS_QUERY: QueryRegistry;
7
+ export declare const botChatFilter: (words: QueryWords) => Partial<Pick<SeenChat, "id" | "title" | "kind">>;
8
+ export declare const BOT_MESSAGES_QUERY: QueryRegistry;
9
+ export declare const botMessageWindow: (words: QueryWords) => {
10
+ window: {
11
+ before?: string | undefined;
12
+ since?: string | undefined;
13
+ threadId?: string | undefined;
14
+ outgoing?: boolean | undefined;
15
+ starred?: boolean | undefined;
16
+ };
17
+ meta: {
18
+ timezone: string;
19
+ window: {
20
+ since?: string;
21
+ until?: string;
22
+ };
23
+ } | undefined;
24
+ };
25
+ export declare const BOT_ADMINS_QUERY: QueryRegistry;
26
+ export declare const BOT_MENU_QUERY: QueryRegistry;
27
+ export declare const BOT_WEBHOOKS_QUERY: QueryRegistry;
@@ -0,0 +1,83 @@
1
+ import { queryError, queryFieldsOf } from "@wirecat/cli-base/query";
2
+ import { botAdmin, botMenuEntry, botProfile, botSeenChat, botWebhook, message } from "../../entities/index.js";
3
+ import { listQuery, nativeTerms, queryTerms, timeBounds, typedQuery, unfilteredQuery, } from "../list-query.js";
4
+ const termsOnly = (registry) => ({
5
+ ...registry,
6
+ fields: registry.fields.map((field) => field.native
7
+ ? {
8
+ ...field,
9
+ operators: field.operators.filter((operator) => operator === "term" || operator === "phrase"),
10
+ }
11
+ : field),
12
+ });
13
+ export const BOT_PROFILES_QUERY = termsOnly({
14
+ entity: "bot_profile",
15
+ backend: "native",
16
+ fields: queryFieldsOf(botProfile, { native: ["name"], examples: { name: "name:sales" } }),
17
+ });
18
+ export const botProfileName = (words) => queryTerms(words, BOT_PROFILES_QUERY, "bot list").get("name");
19
+ export const BOT_CHATS_QUERY = termsOnly({
20
+ entity: "bot_seen_chat",
21
+ backend: "native",
22
+ fields: queryFieldsOf(botSeenChat, {
23
+ native: ["id", "title", "kind"],
24
+ examples: { id: "id:7", title: 'title:"Book club"', kind: "kind:group" },
25
+ }),
26
+ });
27
+ export const botChatFilter = (words) => {
28
+ const terms = queryTerms(words, BOT_CHATS_QUERY, "bot chats list");
29
+ const id = terms.get("id"), title = terms.get("title");
30
+ const kind = botSeenChat.entries.kind.wrapped.options.find((value) => value === terms.get("kind"));
31
+ return {
32
+ ...(id === undefined ? {} : { id }),
33
+ ...(title === undefined ? {} : { title }),
34
+ ...(kind === undefined ? {} : { kind }),
35
+ };
36
+ };
37
+ export const BOT_MESSAGES_QUERY = {
38
+ entity: "message",
39
+ backend: "native",
40
+ fields: queryFieldsOf(message, {
41
+ native: ["id", "timestamp", "thread_id", "outgoing", "starred"],
42
+ examples: {
43
+ id: "id<42",
44
+ timestamp: "timestamp>2026-10-01",
45
+ thread_id: "thread_id:12",
46
+ outgoing: "outgoing:false",
47
+ starred: "starred:true",
48
+ },
49
+ }).map((field) => {
50
+ if (field.name === "id" || field.name === "timestamp")
51
+ return { ...field, operators: ["range"] };
52
+ return field.native ? { ...field, operators: ["term"] } : field;
53
+ }),
54
+ };
55
+ export const botMessageWindow = (words) => {
56
+ const command = "bot messages list";
57
+ const terms = nativeTerms(listQuery(typedQuery(words), BOT_MESSAGES_QUERY, command), command);
58
+ const id = terms.get("id");
59
+ if (id && (id.value !== "*" || !id.upper || id.upper === "*" || id.upper_inclusive !== false))
60
+ queryError("query.unsupported-operator", id.span, "id takes an exclusive upper bound here: id<42", { command });
61
+ const timestamp = terms.get("timestamp");
62
+ const bounds = timeBounds(timestamp);
63
+ if (timestamp && (bounds.since === undefined || bounds.until !== undefined))
64
+ queryError("query.unsupported-operator", timestamp.span, "timestamp takes a lower bound here: timestamp>7d", {
65
+ command,
66
+ });
67
+ const topic = terms.get("thread_id")?.value;
68
+ const outgoing = terms.get("outgoing")?.value.toLowerCase();
69
+ const starred = terms.get("starred")?.value.toLowerCase();
70
+ return {
71
+ window: {
72
+ ...(id?.upper === undefined ? {} : { before: id.upper }),
73
+ ...(bounds.since === undefined ? {} : { since: new Date(bounds.since).toISOString() }),
74
+ ...(topic === undefined ? {} : { threadId: topic }),
75
+ ...(outgoing === undefined ? {} : { outgoing: outgoing === "true" }),
76
+ ...(starred === undefined ? {} : { starred: starred === "true" }),
77
+ },
78
+ meta: bounds.meta,
79
+ };
80
+ };
81
+ export const BOT_ADMINS_QUERY = unfilteredQuery("bot_admin", botAdmin);
82
+ export const BOT_MENU_QUERY = unfilteredQuery("bot_menu_entry", botMenuEntry);
83
+ export const BOT_WEBHOOKS_QUERY = unfilteredQuery("bot_webhook", botWebhook);
@@ -9,12 +9,14 @@ import { readAttachments } from "../../sends/upload.js";
9
9
  import { environmentOf } from "../context.js";
10
10
  import { ownerOnlyOption, REMOTE_READ, REMOTE_WRITE } from "../effects.js";
11
11
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
12
+ import { typedQuery, withQuery } from "../list-query.js";
12
13
  import { wholeList } from "../messenger/chat-answers.js";
13
14
  import { messageItem } from "../messenger/message-items.js";
14
15
  import { readAll } from "../messenger/stdin.js";
15
16
  import { positiveCount } from "../paging.js";
16
17
  import { botContext } from "./context.js";
17
18
  import { botExamples } from "./examples.js";
19
+ import { BOT_MESSAGES_QUERY, botMessageWindow } from "./list-queries.js";
18
20
  /** The adapter's `method`, or a refusal naming what this messenger's bot cannot do. */
19
21
  export const botCan = (adapter, method, bot, what) => {
20
22
  const found = adapter[method];
@@ -32,10 +34,10 @@ export const botIdOf = async (context, adapter) => {
32
34
  context.registry.rememberBot(id);
33
35
  return id;
34
36
  };
35
- const show = (command, context, bot, botId, messages, one = false) => {
37
+ const show = (command, context, bot, botId, messages, one = false, meta) => {
36
38
  const reader = context.copy.accountOf(botId);
37
39
  const items = messages.map((message) => messageItem(message, reader));
38
- renderAnswer(command, context, one && items[0] ? itemOf(items[0]) : wholeList("bot messages list", items), () => renderMessages(messages, {
40
+ renderAnswer(command, context, one && items[0] ? itemOf(items[0]) : { ...wholeList("bot messages list", items), ...(meta ? { meta } : {}) }, () => renderMessages(messages, {
39
41
  color: context.color,
40
42
  verbosity: context.settings.detail,
41
43
  senderColors: context.settings.senderColors,
@@ -124,33 +126,37 @@ const sendCommand = (bot) => annotate(new Command("send"), {
124
126
  renderAnswer(this, context, written(context, botId, { operationId }, message));
125
127
  });
126
128
  });
127
- const listCommand = (bot) => annotate(new Command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "messages list Team --limit 20") })
129
+ const listCommand = (bot) => withQuery(annotate(new Command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "messages list Team --limit 20") })
128
130
  .description(`the latest messages in a chat; where ${bot.name ?? "the messenger"} gives a bot no history, and with --offline, ` +
129
131
  "the ones this bot has seen on this machine")
130
132
  .summary("the latest messages in a chat")
131
- .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
133
+ .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen"), BOT_MESSAGES_QUERY)
134
+ .addHelpText("after", "\nQueries filter the stored copy before limiting results; add --offline. The Bot API does not accept these filters.")
132
135
  .option("--limit <n>", "how many, the newest", positiveCount("--limit"))
133
- .action(async function (chat) {
136
+ .action(async function (chat, words) {
137
+ const narrowed = botMessageWindow(words);
134
138
  const context = botContext(this, bot);
139
+ if (typedQuery(words).trim() && !context.settings.offline)
140
+ throw new CliError("validation_error", "bot message queries filter the local copy; add --offline");
135
141
  const ref = context.chatRef(chat);
136
142
  const limit = this.opts().limit ?? context.settings.limit;
137
143
  await context.run(async (events) => {
138
144
  const fromCopy = async (botId) => {
139
- const page = await context.copy.read((store) => store.messages.messages(context.copy.accountOf(botId), ref, { limit }));
145
+ const page = await context.copy.read((store) => store.messages.messages(context.copy.accountOf(botId), ref, { limit, ...narrowed.window }));
140
146
  return page.items;
141
147
  };
142
148
  if (context.settings.offline) {
143
149
  const botId = context.registry.botId();
144
150
  if (!botId)
145
151
  throw nothingKept(context);
146
- show(this, context, bot, botId, await fromCopy(botId));
152
+ show(this, context, bot, botId, await fromCopy(botId), false, narrowed.meta);
147
153
  return;
148
154
  }
149
155
  const adapter = await context.authenticated({ events });
150
156
  const botId = await botIdOf(context, adapter);
151
157
  if (!adapter.history) {
152
158
  context.renderer.note(`${bot.name ?? "the messenger"} gives a bot no history: these are the messages it kept`);
153
- show(this, context, bot, botId, await fromCopy(botId));
159
+ show(this, context, bot, botId, await fromCopy(botId), false, narrowed.meta);
154
160
  return;
155
161
  }
156
162
  const messages = await adapter.history(ref, { limit });
@@ -24,7 +24,7 @@ export declare const botFiles: (app: AppIdentity, profile: string, env?: NodeJS.
24
24
  export declare class ChatRegistry {
25
25
  #private;
26
26
  constructor(app: AppIdentity, profile: string, env?: NodeJS.ProcessEnv);
27
- list(): SeenChat[];
27
+ list(filter?: Partial<Pick<SeenChat, "id" | "title" | "kind">>): SeenChat[];
28
28
  /** The bot's own user id, once any command has learned it — the key its messages are stored under. */
29
29
  botId(): string | undefined;
30
30
  rememberBot(id: string): void;
@@ -26,9 +26,11 @@ export class ChatRegistry {
26
26
  constructor(app, profile, env = process.env) {
27
27
  this.#path = botFiles(app, profile, env).registry;
28
28
  }
29
- list() {
29
+ list(filter = {}) {
30
30
  const { chats } = this.#read();
31
- return Array.isArray(chats) ? chats : [];
31
+ return (Array.isArray(chats) ? chats : []).filter((chat) => (filter.id === undefined || chat.id === filter.id) &&
32
+ (filter.kind === undefined || chat.kind === filter.kind) &&
33
+ (filter.title === undefined || chat.title?.toLocaleLowerCase() === filter.title?.toLocaleLowerCase()));
32
34
  }
33
35
  /** The bot's own user id, once any command has learned it — the key its messages are stored under. */
34
36
  botId() {
@@ -5,9 +5,11 @@ import { guardedWrite } from "../../sends/guarded.js";
5
5
  import { newOperationId } from "../../sends/send-id.js";
6
6
  import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
7
7
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
8
+ import { listQuery, typedQuery, withQuery } from "../list-query.js";
8
9
  import { wholeList } from "../messenger/chat-answers.js";
9
10
  import { botContext, online } from "./context.js";
10
11
  import { botExamples } from "./examples.js";
12
+ import { BOT_MENU_QUERY, BOT_WEBHOOKS_QUERY } from "./list-queries.js";
11
13
  import { botCan } from "./messages.js";
12
14
  import { botFiles } from "./registry.js";
13
15
  import { PressLog } from "./updates.js";
@@ -31,9 +33,10 @@ const botWrite = (context, key, write) => guardedWrite(context.guard(), { operat
31
33
  /** `bot commands`: the menu people see when they type `/`. */
32
34
  export const botMenuCommand = (bot) => {
33
35
  const command = new Command("commands").description("the bot's command menu — what people see after /");
34
- annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "commands list") })
36
+ withQuery(annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "commands list") }), BOT_MENU_QUERY)
35
37
  .description("the commands in the menu now")
36
- .action(async function () {
38
+ .action(async function (words) {
39
+ listQuery(typedQuery(words), BOT_MENU_QUERY, "bot commands list");
37
40
  const context = online(botContext(this, bot), this);
38
41
  await context.run(async (events) => {
39
42
  const adapter = await context.authenticated({ events });
@@ -111,9 +114,10 @@ export const botWebhooksCommand = (bot) => {
111
114
  const command = new Command("webhooks")
112
115
  .description("where the messenger pushes this bot's updates — while one is set, `bot watch` gets nothing")
113
116
  .summary("where the messenger pushes this bot's updates");
114
- annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "webhooks list") })
117
+ withQuery(annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "webhooks list") }), BOT_WEBHOOKS_QUERY)
115
118
  .description("the webhooks this bot has")
116
- .action(async function () {
119
+ .action(async function (words) {
120
+ listQuery(typedQuery(words), BOT_WEBHOOKS_QUERY, "bot webhooks list");
117
121
  const context = online(botContext(this, bot), this);
118
122
  await context.run(async (events) => {
119
123
  const adapter = await context.authenticated({ events });
@@ -0,0 +1,14 @@
1
+ import type { CommandInfo } from "@wirecat/cli-base/commands";
2
+ import { type Level, type PermissionKey } from "../sends/permissions.js";
3
+ export type AvailabilityCommand = Pick<CommandInfo, "path" | "effects" | "lifetime" | "reaches">;
4
+ export interface DiscoveredCommand extends CommandInfo {
5
+ availability?: CommandAvailability;
6
+ commands: readonly DiscoveredCommand[];
7
+ }
8
+ export interface CommandAvailability {
9
+ available: boolean;
10
+ reason?: string;
11
+ permission?: string;
12
+ }
13
+ /** Discovery and execution use the same permission decision. */
14
+ export declare const commandAvailability: (info: AvailabilityCommand, levels: Readonly<Record<PermissionKey, Level>>, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined, authority?: (path: readonly string[]) => boolean, readOnly?: boolean, mcp?: boolean) => CommandAvailability;
@@ -0,0 +1,25 @@
1
+ import { writes } from "@wirecat/cli-base/standards";
2
+ import { AUTHORITY_KEY, authorityAllowed, BOT_API_KEY, botApiAllowed, isBotApi, keyForCommand, levelFor, readKeysForCommand, } from "../sends/permissions.js";
3
+ import { changesMessenger } from "./effects.js";
4
+ /** Discovery and execution use the same permission decision. */
5
+ export const commandAvailability = (info, levels, keyOf, authority, readOnly = false, mcp = true) => {
6
+ const blocked = (reason, permission) => ({
7
+ available: false,
8
+ reason,
9
+ ...(permission ? { permission } : {}),
10
+ });
11
+ if (readOnly && (changesMessenger(info) || authority?.(info.path)))
12
+ return blocked("the read-only lock blocks this change");
13
+ if (mcp && authority?.(info.path) && !authorityAllowed(levels))
14
+ return blocked(`the profile keeps ${AUTHORITY_KEY} off`, AUTHORITY_KEY);
15
+ if (mcp && isBotApi(info.path) && !botApiAllowed(levels))
16
+ return blocked(`the profile requires ${BOT_API_KEY}: allow`, BOT_API_KEY);
17
+ const key = keyOf?.(info.path) ?? keyForCommand(info.path);
18
+ const level = key ? levelFor(levels, key).level : "allow";
19
+ if (level === "deny")
20
+ return blocked(`the profile denies ${key}`, key ?? undefined);
21
+ if (level === "readonly" && writes(info.effects))
22
+ return blocked(`${key} is readonly in this profile`, key ?? undefined);
23
+ const denied = readKeysForCommand(info.path).find((read) => levelFor(levels, read).level === "deny");
24
+ return denied ? blocked(`the profile denies ${denied}`, denied) : { available: true };
25
+ };
@@ -7,6 +7,6 @@ import type { AppIdentity } from "../ports/app.js";
7
7
  export declare const CONTRACT = 3;
8
8
  /**
9
9
  * The discovery surface an agent reads instead of `--help`. It opens no store, no session and no
10
- * settings file: which commands exist does not depend on whether this machine has logged in.
10
+ * settings file without a named profile. A named profile adds availability without hiding commands.
11
11
  */
12
12
  export declare const commandsCommand: (app: AppIdentity) => Command;
@@ -1,8 +1,8 @@
1
1
  import { CliError, EXIT_CODES, GENERIC_FAILURE } from "@wirecat/cli-base";
2
- import { annotate, describeOptions, describeProgram } from "@wirecat/cli-base/commands";
2
+ import { annotate, describeOptions, describeProgram, metaOf } from "@wirecat/cli-base/commands";
3
3
  import { CRASH_CODE } from "@wirecat/cli-base/standards";
4
4
  import { Command } from "commander";
5
- import { commandContract, findCommand } from "./command-contract.js";
5
+ import { commandContract, commandPathOf, findCommand } from "./command-contract.js";
6
6
  import { environmentOf, outputFor } from "./context.js";
7
7
  import { NO_EFFECTS } from "./effects.js";
8
8
  import { renderDocument } from "./envelope.js";
@@ -15,7 +15,7 @@ import { rootOf } from "./profile.js";
15
15
  export const CONTRACT = 3;
16
16
  /**
17
17
  * The discovery surface an agent reads instead of `--help`. It opens no store, no session and no
18
- * settings file: which commands exist does not depend on whether this machine has logged in.
18
+ * settings file without a named profile. A named profile adds availability without hiding commands.
19
19
  */
20
20
  export const commandsCommand = (app) => {
21
21
  const command = annotate(new Command("commands"), { effects: NO_EFFECTS })
@@ -30,7 +30,12 @@ export const commandsCommand = (app) => {
30
30
  .action(function (path) {
31
31
  const root = rootOf(this);
32
32
  const output = outputFor(this);
33
- const all = describeProgram(root);
33
+ const available = environmentOf(this).discoveryAvailability;
34
+ const annotateAvailability = (infos) => infos.map((info) => {
35
+ const availability = available?.(info);
36
+ return { ...info, ...(availability ? { availability } : {}), commands: annotateAvailability(info.commands) };
37
+ });
38
+ const all = annotateAvailability(describeProgram(root));
34
39
  const scoped = path.length > 0 ? scopeOf(root, all, path) : undefined;
35
40
  const commands = scoped ? [scoped.command] : all;
36
41
  const { json, jsonl } = this.optsWithGlobals();
@@ -62,6 +67,14 @@ export const commandsCommand = (app) => {
62
67
  version: app.version,
63
68
  contract: CONTRACT,
64
69
  ...commandContract(findCommand(rootOf(this), path)),
70
+ ...(() => {
71
+ const found = findCommand(rootOf(this), path);
72
+ const availability = environmentOf(this).discoveryAvailability?.({
73
+ ...metaOf(found),
74
+ path: commandPathOf(found),
75
+ });
76
+ return availability ? { availability } : {};
77
+ })(),
65
78
  });
66
79
  }));
67
80
  return command;
@@ -11,6 +11,7 @@ import { type Closeable } from "./deadline.js";
11
11
  import type { GlobalFlags, ResolveOptions, Settings } from "./settings.js";
12
12
  /** What every CLI's commands write to, when it is not the real terminal. A CLI adds its own fields. */
13
13
  export interface BaseEnvironment {
14
+ discoveryAvailability?: (command: import("./command-availability.js").AvailabilityCommand) => import("./command-availability.js").CommandAvailability | undefined;
14
15
  streams?: Streams;
15
16
  /** Whether a person is looking. Defaults to whether stdout is a terminal. */
16
17
  tty?: boolean;
@@ -14,7 +14,7 @@ export declare const NO_EFFECTS: Effects;
14
14
  /**
15
15
  * Marks a local write that changes what a later run may do: a schedule, a setting, the recipient list, an
16
16
  * auto-reply, an agent's setup. `<PREFIX>_READ_ONLY` refuses it, or an agent under the lock could hand an
17
- * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server offers it only where the
17
+ * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server permits execution only where the
18
18
  * profile allows `mcp.authority`, which is off until the owner turns it on (NEED-1662).
19
19
  * `unless` names a flag that makes the run only look (`upgrade --check`), which the lock lets through.
20
20
  */
@@ -16,7 +16,7 @@ const AUTHORITY = new WeakMap();
16
16
  /**
17
17
  * Marks a local write that changes what a later run may do: a schedule, a setting, the recipient list, an
18
18
  * auto-reply, an agent's setup. `<PREFIX>_READ_ONLY` refuses it, or an agent under the lock could hand an
19
- * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server offers it only where the
19
+ * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server permits execution only where the
20
20
  * profile allows `mcp.authority`, which is off until the owner turns it on (NEED-1662).
21
21
  * `unless` names a flag that makes the run only look (`upgrade --check`), which the lock lets through.
22
22
  */
@@ -1,6 +1,7 @@
1
1
  import { metaOf } from "@wirecat/cli-base/commands";
2
2
  import { writes } from "@wirecat/cli-base/standards";
3
3
  import { commandPathOf, outputOf } from "../command-contract.js";
4
+ import { environmentOf } from "../context.js";
4
5
  import { PROFILE_FREE } from "../profile.js";
5
6
  import { commandAt } from "../steps.js";
6
7
  import { short } from "./commands-text.js";
@@ -195,7 +196,23 @@ const LABEL = 15;
195
196
  export const commandHelpText = (command) => {
196
197
  const { returns, next, examples } = commandHelp(command);
197
198
  const rows = (label, values) => values.map((value, at) => `${(at === 0 ? label : "").padEnd(LABEL)}${value}`);
198
- return ["", ...rows("Returns:", [returns]), ...rows("Next usually:", next), ...rows("Example:", examples), ""].join("\n");
199
+ const availability = environmentOf(command).discoveryAvailability?.({
200
+ ...metaOf(command),
201
+ path: commandPathOf(command),
202
+ });
203
+ const access = availability
204
+ ? rows("Availability:", [
205
+ availability.available ? "available for this profile" : `unavailable — ${availability.reason}`,
206
+ ])
207
+ : [];
208
+ return [
209
+ "",
210
+ ...access,
211
+ ...rows("Returns:", [returns]),
212
+ ...rows("Next usually:", next),
213
+ ...rows("Example:", examples),
214
+ "",
215
+ ].join("\n");
199
216
  };
200
217
  /** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
201
218
  export const commandHelpMarkdown = (root, path) => {
@@ -1,5 +1,5 @@
1
- import type { CommandInfo } from "@wirecat/cli-base/commands";
2
1
  import type { Command } from "commander";
2
+ import type { DiscoveredCommand as CommandInfo } from "../command-availability.js";
3
3
  /** The first clause that fits: a line per command is the point, the whole text is in `--help`. */
4
4
  export declare const short: (text: string) => string;
5
5
  /**
@@ -33,10 +33,11 @@ export const commandsText = (root, commands, scope) => {
33
33
  if (infos.length === 0)
34
34
  continue;
35
35
  lines.push("", SECTIONS[section]);
36
- for (const { usage, summary, description, effects } of infos) {
36
+ for (const { usage, summary, description, effects, availability } of infos) {
37
37
  const what = short(summary ?? description);
38
38
  const mark = effects === undefined || writes(effects) ? " (writes)" : "";
39
- lines.push(` ${usage.padEnd(COLUMN)} ${what}${mark}`.trimEnd());
39
+ const access = availability?.available === false ? ` [unavailable: ${availability.reason}]` : "";
40
+ lines.push(` ${usage.padEnd(COLUMN)} ${what}${mark}${access}`.trimEnd());
40
41
  }
41
42
  }
42
43
  return `${lines.join("\n")}\n`;
@@ -1,5 +1,6 @@
1
1
  export { AI_ENTRIES, AI_SETTING_KEYS, type AISettings, resolveAISettings } from "../analysis/settings.js";
2
2
  export { parseDuration } from "../domain/duration.js";
3
+ export { botTemplate } from "../entities/list-results.js";
3
4
  export { syncProgress } from "../entities/store-records.js";
4
5
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
5
6
  export { type AppErrors, failureContext } from "../errors/reasons.js";
@@ -49,6 +50,7 @@ export { BOT_ACTIONS, type BotAction, type BotAdapter, type BotCallbacks, type B
49
50
  export { botFiles, botsDirectory, ChatRegistry, registryProfiles, type SeenChat } from "./bot/registry.js";
50
51
  export { BotTokenStore, type BotTokenStoreOptions } from "./bot/token.js";
51
52
  export { PressLog, UpdatesCursor } from "./bot/updates.js";
53
+ export type { CommandAvailability, DiscoveredCommand } from "./command-availability.js";
52
54
  export { commandContract, type JsonSchema, SCHEMA_DIALECT } from "./command-contract.js";
53
55
  export { CONTRACT, commandsCommand } from "./commands-command.js";
54
56
  export { configCommand, refuseUnknownKey } from "./config-command.js";
package/dist/cli/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AI_ENTRIES, AI_SETTING_KEYS, resolveAISettings } from "../analysis/settings.js";
2
2
  export { parseDuration } from "../domain/duration.js";
3
+ export { botTemplate } from "../entities/list-results.js";
3
4
  export { syncProgress } from "../entities/store-records.js";
4
5
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
5
6
  export { failureContext } from "../errors/reasons.js";