@wirecat/cli-core 0.405.0 → 0.406.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 (112) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/botcheck/reasons.d.ts +1 -1
  3. package/dist/cli/analysis-choice.js +1 -1
  4. package/dist/cli/answer-entity.d.ts +5 -0
  5. package/dist/cli/answer-entity.js +25 -0
  6. package/dist/cli/command-contract.js +2 -5
  7. package/dist/cli/commands-command.js +4 -1
  8. package/dist/cli/effects.js +1 -1
  9. package/dist/cli/embedding-choice.d.ts +1 -1
  10. package/dist/cli/embedding-choice.js +2 -2
  11. package/dist/cli/help/command-help.js +2 -2
  12. package/dist/cli/index.d.ts +1 -0
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/json-fields.js +2 -2
  15. package/dist/cli/meeting-project-candidates-command.js +0 -1
  16. package/dist/cli/meeting-series-inventory-command.js +0 -1
  17. package/dist/cli/messenger/admin-commands.js +14 -14
  18. package/dist/cli/messenger/archive-commands.js +2 -2
  19. package/dist/cli/messenger/attachments-command.js +7 -8
  20. package/dist/cli/messenger/backfill-command.js +1 -1
  21. package/dist/cli/messenger/chat-answers.js +3 -3
  22. package/dist/cli/messenger/chat-lists.js +3 -3
  23. package/dist/cli/messenger/chats-command.js +8 -8
  24. package/dist/cli/messenger/chats-members-command.js +1 -1
  25. package/dist/cli/messenger/contacts-command.js +10 -7
  26. package/dist/cli/messenger/context.d.ts +11 -4
  27. package/dist/cli/messenger/context.js +6 -0
  28. package/dist/cli/messenger/conversation-examples.js +7 -6
  29. package/dist/cli/messenger/conversations-command.d.ts +0 -4
  30. package/dist/cli/messenger/conversations-command.js +130 -135
  31. package/dist/cli/messenger/counters-command.js +4 -2
  32. package/dist/cli/messenger/doctor-command.js +8 -4
  33. package/dist/cli/messenger/gaps-command.js +2 -2
  34. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  35. package/dist/cli/messenger/hearing-command.js +1 -1
  36. package/dist/cli/messenger/images-command.js +1 -1
  37. package/dist/cli/messenger/inbox.js +1 -3
  38. package/dist/cli/messenger/lacks.d.ts +1 -0
  39. package/dist/cli/messenger/lacks.js +1 -1
  40. package/dist/cli/messenger/mcp-command.js +4 -4
  41. package/dist/cli/messenger/messages-comments-command.js +10 -5
  42. package/dist/cli/messenger/metadata-command.js +5 -5
  43. package/dist/cli/messenger/models-command.js +101 -90
  44. package/dist/cli/messenger/private-people-command.js +5 -6
  45. package/dist/cli/messenger/replies-edit-command.js +0 -1
  46. package/dist/cli/messenger/review.js +1 -3
  47. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  48. package/dist/cli/messenger/stats-command.js +1 -1
  49. package/dist/cli/messenger/store-maintenance-command.d.ts +2 -2
  50. package/dist/cli/messenger/store-maintenance-command.js +4 -4
  51. package/dist/cli/messenger/stored.js +5 -0
  52. package/dist/cli/messenger/tags-command.js +4 -10
  53. package/dist/cli/messenger/tasks-command.js +3 -3
  54. package/dist/cli/messenger/transcribe-command.js +1 -1
  55. package/dist/cli/moved.d.ts +2 -0
  56. package/dist/cli/moved.js +39 -1
  57. package/dist/cli/permission-keys.js +2 -0
  58. package/dist/cli/settings.js +1 -1
  59. package/dist/cli/steps.js +4 -4
  60. package/dist/cli/store-settings.js +1 -1
  61. package/dist/cli/upgrade-command.js +0 -1
  62. package/dist/domain/models.d.ts +3 -3
  63. package/dist/domain/task-categories.js +1 -1
  64. package/dist/embeddings/remote.js +1 -1
  65. package/dist/entities/member.d.ts +2 -2
  66. package/dist/entities/member.js +2 -2
  67. package/dist/mcp/agent.d.ts +1 -0
  68. package/dist/mcp/agent.js +1 -1
  69. package/dist/mcp/tools/attachments.js +1 -1
  70. package/dist/meetings/mcp/index.d.ts +15 -15
  71. package/dist/ports/app.d.ts +1 -1
  72. package/dist/ports/messenger.d.ts +6 -1
  73. package/dist/search/task-similarity.js +1 -1
  74. package/dist/sends/guard.js +2 -2
  75. package/dist/sends/permission-aliases.d.ts +5 -0
  76. package/dist/sends/permission-aliases.js +49 -0
  77. package/dist/sends/permissions.js +33 -25
  78. package/dist/services/admin.js +7 -7
  79. package/dist/services/chats.js +1 -1
  80. package/dist/services/conversations.d.ts +1 -1
  81. package/dist/services/conversations.js +1 -1
  82. package/dist/services/counters.js +1 -1
  83. package/dist/services/embeddings-maintenance.js +2 -2
  84. package/dist/services/embeddings-search.js +1 -1
  85. package/dist/services/embeddings.d.ts +4 -4
  86. package/dist/services/messages-search.d.ts +1 -1
  87. package/dist/services/messages-search.js +2 -2
  88. package/dist/services/metadata.js +2 -2
  89. package/dist/services/notes-search.js +3 -3
  90. package/dist/services/search-catchup.js +1 -1
  91. package/dist/speech/install.d.ts +4 -2
  92. package/dist/speech/install.js +16 -4
  93. package/dist/speech/local.js +1 -1
  94. package/dist/speech/transcribe.js +1 -1
  95. package/dist/store/search-index.d.ts +1 -1
  96. package/dist/store/sqlite/attachment-texts.d.ts +1 -1
  97. package/dist/store/sqlite/attachment-texts.js +1 -1
  98. package/dist/store/sqlite/batches.js +1 -1
  99. package/dist/store/sqlite/involvements.d.ts +1 -1
  100. package/dist/store/sqlite/note-index.d.ts +2 -2
  101. package/dist/store/sqlite/note-index.js +2 -2
  102. package/dist/store/sqlite/search-index.d.ts +1 -1
  103. package/dist/store/sqlite/search-index.js +1 -1
  104. package/dist/store/sqlite/stems-fill.d.ts +3 -3
  105. package/dist/store/sqlite/stems-fill.js +3 -3
  106. package/dist/store/sqlite/stems.d.ts +1 -1
  107. package/dist/store/sqlite/task-pairs.d.ts +1 -1
  108. package/dist/store/sqlite/task-pairs.js +1 -1
  109. package/package.json +1 -1
  110. package/parity.json +130 -139
  111. package/skills/follow-ups/SKILL.md +5 -5
  112. package/skills/link-conversations/SKILL.md +8 -8
package/CHANGELOG.md CHANGED
@@ -5,7 +5,30 @@ 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.406.0 — 24.10.2026
9
+
10
+ ### Added
11
+
12
+ - `StoredMessages.threadMessages` lets an adapter read a provider thread from the shared message store without
13
+ keeping another copy. `HistoryBatch.deletions` applies chat-scoped tombstones from synced snapshots without
14
+ replaying those deletions as live events. `Messenger.readsComments` allows comment reads independently of
15
+ comment sending; unset, it keeps the existing capability behavior.
16
+ - Speech model installation can verify existing files before reusing them (`verifyExisting`), so repair uses the shared checksum and download path.
17
+ - Provider adapters can declare a typed answer with `answerEntity`, shared by command schemas, help and JSON field validation without changing a shared entity.
18
+
19
+ ### Fixed
20
+
21
+ - Finite meeting inventory, project scanning and upgrade commands are available to the shared MCP catalogue. Profile permissions still apply; upgrade retains its authority grant.
22
+
23
+ ### Changed — may break callers
24
+
25
+ - **Only canonical option spellings are accepted.** The hidden flag aliases introduced in 0.399.0 are removed: use the analysis/embedding/consent provider flags, `--claim`, `--answer-model`, `--template-mode`, `--text-file`, `--disappear-after` and the `kind:` query term. Retired flags are refused before command work.
26
+
27
+ - **Shared command paths use the vocabulary.** Contacts use `info`, `events` and `aliases remove`; chat invite links use `links remove`, member checks use `check`, and join requests use `confirm` or `reject`. Metadata uses `show` and `sync`; task edits use `update`; derived tags and search indexes use `rebuild`. Discovery uses `commands info`; MCP configuration uses `mcp add` and `mcp check`.
28
+
29
+ - **Explicit model and conversation commands.** Use `models list kind:audio` or `kind:text`, `models download <model>` and `models keys set/remove`. Conversation link construction is `conversations links rebuild`; vector reports and removal are `conversations embeddings status/clear`, while `conversations embed` still computes vectors. Duplicate candidates are `tasks duplicates list`. Report information is `doctor reports show`; `doctor reports create` writes the report. Bare groups only offer help. Retired spellings are refused with guidance to the canonical path; no compatibility command aliases are mounted. CLI and MCP use the same paths.
30
+
31
+ - **Rename moved permission paths.** A saved key or startup override using a renamed path now stops with `configuration_error` and its exact replacement (including bot keys), before opening the store or performing the action. Canonical commands keep the migrated denial; retired CLI spellings cannot run.
9
32
 
10
33
  ## 0.405.0 — 24.10.2026
11
34
 
@@ -1,4 +1,4 @@
1
- /** What `chats members audit` judges from a member list and the store. */
1
+ /** What `chats members check` judges from a member list and the store. */
2
2
  export type AuditReason = "bot" | "scam" | "fake" | "deleted" | "no_photo" | "no_username" | "odd_name" | "never_wrote" | "link_first" | "burst_join" | "mass_invited";
3
3
  /** What only a look at one person adds: their profile, all their stored messages, the registries. */
4
4
  export type PersonReason = "new_account" | "no_bio" | "same_text" | "photo_recent" | RegistryReason;
@@ -35,6 +35,6 @@ export const analysisChoice = (options, app, settings, env, keyring) => {
35
35
  const keyName = customUrl === undefined ? provider : endpointKeyName(baseUrl);
36
36
  const apiKey = embeddingKeys(app, env, keyring).read(keyName)?.key;
37
37
  if (!apiKey && customUrl === undefined)
38
- throw new CliError("authentication_error", `no ${provider} key — use models text key set ${provider}`);
38
+ throw new CliError("authentication_error", `no ${provider} key — use models keys set ${provider}`);
39
39
  return { provider, model, baseUrl, ...(apiKey ? { apiKey } : {}) };
40
40
  };
@@ -0,0 +1,5 @@
1
+ import type { Command } from "commander";
2
+ import type * as v from "valibot";
3
+ /** Provider-specific wire fields must not redefine an entity shared by every CLI. */
4
+ export declare const answerEntity: (command: Command, name: string, schema: v.GenericSchema) => Command;
5
+ export declare const answerEntitySchema: (command: Command | undefined) => v.GenericSchema | undefined;
@@ -0,0 +1,25 @@
1
+ import { annotate, metaOf } from "@wirecat/cli-base/commands";
2
+ import { ENTITIES } from "../entities/index.js";
3
+ const schemas = new WeakMap();
4
+ /** Provider-specific wire fields must not redefine an entity shared by every CLI. */
5
+ export const answerEntity = (command, name, schema) => {
6
+ if (Object.hasOwn(ENTITIES, name))
7
+ throw new Error(`${name} is a shared entity; use its existing schema`);
8
+ if (!/^[a-z][a-z0-9_]*$/.test(name))
9
+ throw new Error("an answer entity needs a lowercase name");
10
+ if (!("entries" in schema) && schema.type !== "union")
11
+ throw new Error("an answer entity needs an object schema or a union of object schemas");
12
+ schemas.set(command, { name, schema });
13
+ return annotate(command, { output: { entity: name } });
14
+ };
15
+ export const answerEntitySchema = (command) => {
16
+ const output = command && metaOf(command).output;
17
+ if (!output)
18
+ return undefined;
19
+ const name = output.view ? `${output.entity}.${output.view}` : output.entity;
20
+ const shared = ENTITIES[name];
21
+ if (shared)
22
+ return shared;
23
+ const own = schemas.get(command);
24
+ return own?.name === name ? own.schema : undefined;
25
+ };
@@ -1,8 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { describeOptions, metaOf } from "@wirecat/cli-base/commands";
3
3
  import { jsonSchemaOf } from "@wirecat/cli-base/standards";
4
- import { ENTITIES } from "../entities/index.js";
5
4
  import { keyForCommand } from "../sends/permissions.js";
5
+ import { answerEntitySchema } from "./answer-entity.js";
6
6
  import { answerSchema } from "./envelope.js";
7
7
  export const SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
8
8
  /**
@@ -10,10 +10,7 @@ export const SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
10
10
  * A command that declares none answers the same envelopes with items of any shape; `--jsonl` streams the items.
11
11
  */
12
12
  export const outputOf = (command) => {
13
- const output = metaOf(command).output;
14
- const entity = output
15
- ? ENTITIES[(output.view ? `${output.entity}.${output.view}` : output.entity)]
16
- : undefined;
13
+ const entity = answerEntitySchema(command);
17
14
  const { $schema: _, ...schema } = jsonSchemaOf(answerSchema(entity));
18
15
  const { $schema: __, ...item } = (entity ? jsonSchemaOf(entity) : {});
19
16
  const coverage = entity ? "declared-entity" : "envelope-with-open-items";
@@ -50,7 +50,10 @@ export const commandsCommand = (app) => {
50
50
  exit_codes: { ok: 0, [CRASH_CODE]: GENERIC_FAILURE, ...EXIT_CODES },
51
51
  });
52
52
  });
53
- command.addCommand(annotate(new Command("schema"), { effects: NO_EFFECTS })
53
+ command.addCommand(annotate(new Command("info"), {
54
+ effects: NO_EFFECTS,
55
+ examples: [`${app.command} commands info messages list`],
56
+ })
54
57
  .description("one command's argv and result schemas, effects, permissions and retry guidance")
55
58
  .argument("<path...>", "one command path, for example: stats messages show")
56
59
  .action(function (path) {
@@ -124,7 +124,7 @@ export const ownerOnlyOption = (flags, description) => annotateOption(new Option
124
124
  export const judgedEffects = (effects) => effects && { remote: effects.remote, local: "none" };
125
125
  /**
126
126
  * An unasked step is judged stricter than a retry: only a fetch's local write is incidental. A command that reads
127
- * nothing remotely and writes here (`server restart`, `store reindex`) is the whole point of the step, so it waits
127
+ * nothing remotely and writes here (`server restart`, `store rebuild`) is the whole point of the step, so it waits
128
128
  * to be asked (RISK-1211).
129
129
  */
130
130
  const judgedStepEffects = (effects) => effects?.remote === "none" ? effects : judgedEffects(effects);
@@ -10,7 +10,7 @@ export interface ModelOptions {
10
10
  dims?: number;
11
11
  concurrency?: number;
12
12
  }
13
- /** A local model id, or a remote model with its key from `models text key set`. */
13
+ /** A local model id, or a remote model with its key from `models keys set`. */
14
14
  export declare const embeddingChoice: ({ model, provider, embeddingProvider, baseUrl, dims, concurrency }: ModelOptions, app: AppIdentity, settings: AISettings, env: NodeJS.ProcessEnv, { needKey, keyring }?: {
15
15
  needKey?: boolean;
16
16
  keyring?: KeyringStore | undefined;
@@ -5,7 +5,7 @@ import { DEFAULT_TEXT_MODEL } from "../embeddings/models.js";
5
5
  import { isLocal, remoteModel } from "../embeddings/remote.js";
6
6
  import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
7
7
  import { typedCommand } from "../runs/scope.js";
8
- /** A local model id, or a remote model with its key from `models text key set`. */
8
+ /** A local model id, or a remote model with its key from `models keys set`. */
9
9
  export const embeddingChoice = ({ model, provider, embeddingProvider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
10
10
  if (embeddingProvider !== undefined && provider !== undefined)
11
11
  throw new CliError("validation_error", "give --embedding-provider or its legacy --provider spelling, not both");
@@ -34,7 +34,7 @@ export const embeddingChoice = ({ model, provider, embeddingProvider, baseUrl, d
34
34
  const keyName = baseUrl === undefined ? "openai" : endpointKeyName(baseUrl);
35
35
  const key = embeddingKeys(app, env, keyring).read(keyName)?.key;
36
36
  if (!key && needKey && baseUrl === undefined) {
37
- throw new CliError("authentication_error", `no OpenAI key — \`${typedCommand(app.command, "models text key set openai")}\`, or OPENAI_API_KEY`);
37
+ throw new CliError("authentication_error", `no OpenAI key — \`${typedCommand(app.command, "models keys set openai")}\`, or OPENAI_API_KEY`);
38
38
  }
39
39
  return { remote, ...(key ? { apiKey: key } : {}), ...(concurrency ? { concurrency } : {}) };
40
40
  };
@@ -112,8 +112,8 @@ const returnsOf = (command) => {
112
112
  const noun = (output.view ? `${output.entity} ${output.view}` : output.entity).replaceAll("_", " ");
113
113
  const more = fields.length > 8 ? `, and ${fields.length - 8} more` : "";
114
114
  const root = rootOf(command);
115
- const schema = commandAt(root, "commands schema")
116
- ? `; every field: ${root.name()} commands schema ${commandPathOf(command).join(" ")}`
115
+ const schema = commandAt(root, "commands info")
116
+ ? `; every field: ${root.name()} commands info ${commandPathOf(command).join(" ")}`
117
117
  : "";
118
118
  return `${noun} — ${fields.slice(0, 8).join(", ")}${more}${schema}`;
119
119
  };
@@ -33,6 +33,7 @@ export { unanswered } from "../services/questions.js";
33
33
  export { openRequestTask } from "../services/task-rules.js";
34
34
  export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
35
35
  export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent, type Observation, } from "./agent-evaluation.js";
36
+ export { answerEntity } from "./answer-entity.js";
36
37
  export { botAdminsCommand, botMembersCommand } from "./bot/admins.js";
37
38
  export { type ApiCommandInput, type ApiCommands, generatedApiCommand } from "./bot/api.js";
38
39
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
package/dist/cli/index.js CHANGED
@@ -28,6 +28,7 @@ export { unanswered } from "../services/questions.js";
28
28
  export { openRequestTask } from "../services/task-rules.js";
29
29
  export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
30
30
  export { evaluateAgent, } from "./agent-evaluation.js";
31
+ export { answerEntity } from "./answer-entity.js";
31
32
  export { botAdminsCommand, botMembersCommand } from "./bot/admins.js";
32
33
  export { generatedApiCommand } from "./bot/api.js";
33
34
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { metaOf } from "@wirecat/cli-base/commands";
3
3
  import { ENTITIES } from "../entities/index.js";
4
+ import { answerEntitySchema } from "./answer-entity.js";
4
5
  const declaredEntity = (command) => {
5
6
  const output = command ? metaOf(command).output : undefined;
6
7
  return output && (output.view ? `${output.entity}.${output.view}` : output.entity);
@@ -37,8 +38,7 @@ export const answerFields = (command, resolve) => {
37
38
  const own = resolve?.(command);
38
39
  if (own !== undefined)
39
40
  return [...own];
40
- const name = declaredEntity(command);
41
- const schema = name === undefined ? undefined : ENTITIES[name];
41
+ const schema = answerEntitySchema(command);
42
42
  return schemaFields(schema);
43
43
  };
44
44
  const PATHS = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(,[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*)*$/;
@@ -31,7 +31,6 @@ export const addMeetingProjectCandidatesCommand = (program, deps) => {
31
31
  cursor: options.cursor,
32
32
  }, deps.signal()))), {
33
33
  origin: "handwritten",
34
- mcp: false,
35
34
  effects: LOCAL_READ,
36
35
  output: { entity: "meeting_project_candidates" },
37
36
  examples: [`${program.name()} meetings projects scan meeting:1/2 --json`],
@@ -31,7 +31,6 @@ export const addMeetingSeriesInventoryCommand = (program, deps) => {
31
31
  origin: "handwritten",
32
32
  effects: REMOTE_READ,
33
33
  offline: false,
34
- mcp: false,
35
34
  output: { entity: "meeting_series_inventory" },
36
35
  examples: [`${program.name()} meetings scheduled --json`],
37
36
  });
@@ -69,14 +69,8 @@ const updateCommand = (messenger) => {
69
69
  update.option("--remove-photo", "take its photo away; not with --photo");
70
70
  for (const key of keys) {
71
71
  const option = new Option(`--${SETTING_FLAGS[key][0]}`, SETTING_FLAGS[key][1]);
72
- if (key === "disappearingSeconds")
73
- option.conflicts("disappearTime");
74
72
  update.addOption(option);
75
73
  }
76
- if (keys.includes("disappearingSeconds"))
77
- update.addOption(new Option("--disappear-time <duration|off>", "compatibility alias for --disappear-after")
78
- .hideHelp()
79
- .conflicts("disappearAfter"));
80
74
  return update.action(async function (chat) {
81
75
  const context = messengerContext(this, messenger);
82
76
  const { title, description, photo, removePhoto, ...rest } = this.opts();
@@ -92,7 +86,7 @@ const updateCommand = (messenger) => {
92
86
  });
93
87
  };
94
88
  const linkCommand = (messenger) => {
95
- const link = new Command("link").description("a group's invite link");
89
+ const link = new Command("links").description("a group's invite link");
96
90
  annotate(link.command("show"), { output: { entity: "chat_invite" }, effects: REMOTE_READ })
97
91
  .description("the invite link, if you may see it")
98
92
  .argument("<chat>", messenger.chatArgument)
@@ -132,10 +126,13 @@ const linkCommand = (messenger) => {
132
126
  ].join(" ")));
133
127
  }));
134
128
  if (messenger.inviteLinkRevoke !== false)
135
- link.addCommand(annotate(new Command("revoke"), { output: { entity: "chat_invite_change" }, effects: REMOTE_WRITE })
129
+ link.addCommand(annotate(new Command("remove"), {
130
+ output: { entity: "chat_invite_change" },
131
+ effects: REMOTE_WRITE,
132
+ })
136
133
  .description("stop one link; for the group's own link, the answer is the new one")
137
134
  .argument("<chat>", messenger.chatArgument)
138
- .argument("<link>", "the link, as `chats link list` shows it")
135
+ .argument("<link>", "the link, as `chats links list` shows it")
139
136
  .action(async function (chat, link) {
140
137
  const context = messengerContext(this, messenger);
141
138
  renderItem(this, context, await context.withServices((services) => services.admin.revokeLink(chat, link)));
@@ -144,7 +141,7 @@ const linkCommand = (messenger) => {
144
141
  link.addCommand(annotate(new Command("update"), { output: { entity: "chat_invite_change" }, effects: REMOTE_WRITE })
145
142
  .description("change one of your invite links, the group's own one too")
146
143
  .argument("<chat>", messenger.chatArgument)
147
- .argument("<link>", "the link, as `chats link list` shows it")
144
+ .argument("<link>", "the link, as `chats links list` shows it")
148
145
  .option("--approval", "who joins by it asks first, and an admin lets them in; it then has no use limit")
149
146
  .option("--no-approval", "anyone with it joins at once")
150
147
  .option("--expire-time <time>", "it stops working then: 2026-09-25T09:00 (local time), or 30m, 2h, 7d from now; `never` takes the expiry away")
@@ -219,11 +216,14 @@ const requestsCommand = (messenger) => {
219
216
  }));
220
217
  const everyone = messenger.joinRequestsAll !== false;
221
218
  const byLink = everyone && messenger.joinRequestsByLink !== false;
222
- for (const [verb, accept, help] of [
223
- ["accept", true, "let them in; the group sees them join"],
224
- ["decline", false, "turn the request away"],
219
+ for (const [accept, help] of [
220
+ [true, "let them in; the group sees them join"],
221
+ [false, "turn the request away"],
225
222
  ]) {
226
- const answer = annotate(new Command(verb), { output: { entity: "chat_request_change" }, effects: REMOTE_WRITE })
223
+ const answer = annotate(new Command(accept ? "confirm" : "reject"), {
224
+ output: { entity: "chat_request_change" },
225
+ effects: REMOTE_WRITE,
226
+ })
227
227
  .description(help)
228
228
  .argument("<chat>", messenger.chatArgument)
229
229
  .argument(everyone ? "[person]" : "<person>", "who asked: an id from `chats requests list`");
@@ -38,7 +38,7 @@ export const storeCommand = (messenger) => {
38
38
  "copies delete": "messages_copy_1",
39
39
  decrypt: "./backup.enc --output ./restored.db",
40
40
  export: '"Book club" --output ./messages.jsonl',
41
- "gaps plan": '"Book club"',
41
+ "gaps show": '"Book club"',
42
42
  "gaps repair": '"Book club" --limit 100',
43
43
  info: "",
44
44
  "jobs cancel": "job-101",
@@ -47,7 +47,7 @@ export const storeCommand = (messenger) => {
47
47
  "jobs retry": "--failed",
48
48
  "jobs show": "",
49
49
  migrate: "",
50
- reindex: "",
50
+ rebuild: "",
51
51
  repair: "--dry-run",
52
52
  reset: "",
53
53
  restore: "./backup.db",
@@ -27,7 +27,7 @@ const count = (value) => {
27
27
  const line = ({ status, ref, attachment, name, chars }) => `${status} ${ref} #${attachment}${name ? ` ${name}` : ""}${chars === undefined ? "" : ` (${chars} characters)`}`;
28
28
  const summary = (run) => [
29
29
  `${run.extracted} extracted`,
30
- run.needsAgent ? `${run.needsAgent} for an agent to read (attachments text set)` : "",
30
+ run.needsAgent ? `${run.needsAgent} for an agent to read (attachments update)` : "",
31
31
  run.failed ? `${run.failed} unreadable` : "",
32
32
  run.unchanged ? `${run.unchanged} unchanged` : "",
33
33
  run.notDownloaded ? `${run.notDownloaded} not downloaded (--download)` : "",
@@ -172,12 +172,11 @@ const showCommand = (messenger) => annotate(new Command("show"), {
172
172
  stop.release();
173
173
  }
174
174
  });
175
- const textCommand = (messenger) => {
176
- const text = new Command("text").description("the text of one file, as an agent read it");
177
- readsStdin(annotate(text.command("set"), {
175
+ const textUpdateCommand = (messenger) => {
176
+ const update = readsStdin(annotate(new Command("update"), {
178
177
  output: { entity: "attachment_text_change" },
179
178
  effects: LOCAL_WRITE,
180
- examples: [`${messenger.app.command} personal attachments text set "Book club" 101 --text-file ./transcript.txt`],
179
+ examples: [`${messenger.app.command} personal attachments update "Book club" 101 --text-file ./transcript.txt`],
181
180
  }), {
182
181
  name: "text",
183
182
  description: "the text of the file",
@@ -190,7 +189,7 @@ const textCommand = (messenger) => {
190
189
  .addOption(ownerOnlyOption("--text-file <path>", "read the text from this file; - or none reads stdin"))
191
190
  .action(async function (chat, message) {
192
191
  const context = messengerContext(this, messenger);
193
- refuseLocalWrite(context, messenger.app.command, "attachments.text.set");
192
+ refuseLocalWrite(context, messenger.app.command, "attachments.update");
194
193
  const { attachment, textFile } = this.opts();
195
194
  const given = textFile === undefined || textFile === "-" ? await readAll(context.stdin) : readFileSync(textFile, "utf8");
196
195
  const done = await context.withServices((services) => services.attachments.setText({
@@ -201,7 +200,7 @@ const textCommand = (messenger) => {
201
200
  }));
202
201
  renderAnswer(this, context, itemOf(snakeKeys(done)), () => `${done.ref} #${done.attachment} ${done.chars} characters kept\n`);
203
202
  });
204
- return text;
203
+ return update;
205
204
  };
206
205
  /** Files of stored messages: what is in them, for `content:` in a search. Nothing here is sent. */
207
206
  export const attachmentsCommand = (messenger) => {
@@ -211,7 +210,7 @@ export const attachmentsCommand = (messenger) => {
211
210
  .addCommand(extractCommand(messenger))
212
211
  .addCommand(listCommand(messenger))
213
212
  .addCommand(showCommand(messenger))
214
- .addCommand(textCommand(messenger));
213
+ .addCommand(textUpdateCommand(messenger));
215
214
  addMediaArchiveCommands(attachments, messenger);
216
215
  return attachments;
217
216
  };
@@ -107,7 +107,7 @@ export const syncCommand = (messenger) => {
107
107
  if (prepare) {
108
108
  validateCatchUpBounds(preparation);
109
109
  refuseLocalWrite(context, messenger.app.command, "conversations.links");
110
- refuseLocalWrite(context, messenger.app.command, "conversations.embed");
110
+ refuseLocalWrite(context, messenger.app.command, "conversations.embeddings");
111
111
  }
112
112
  if (background) {
113
113
  const named = chat ?? ALL_CHATS;
@@ -182,19 +182,19 @@ export const chatShown = (account, card) => {
182
182
  };
183
183
  /** The lowest score `score>=n` asks for; the audit's own floor when the query names none. */
184
184
  export const scoreFloor = (ast) => {
185
- const term = nativeTerms(ast, "chats members audit").get("score");
185
+ const term = nativeTerms(ast, "chats members check").get("score");
186
186
  if (!term)
187
187
  return AUDIT_MIN_SCORE;
188
188
  const upper = term.operator === "range" ? (term.upper ?? "*") : term.value;
189
189
  if (term.operator !== "range" || upper !== "*")
190
190
  queryError("query.unsupported-operator", term.span, "score takes a lower bound: score>=3", {
191
- command: "chats members audit",
191
+ command: "chats members check",
192
192
  guide: LIST_QUERY_GUIDE,
193
193
  });
194
194
  const floor = Number(term.value);
195
195
  return Math.ceil(term.lower_inclusive === false ? floor + 1 : floor);
196
196
  };
197
- export const auditAnswer = (audit) => wholeList("chats members audit", audit.items.map(memberAnswer), {
197
+ export const auditAnswer = (audit) => wholeList("chats members check", audit.items.map(memberAnswer), {
198
198
  ...(audit.more ? { meta: { truncated: true } } : {}),
199
199
  ...(audit.completeness && audit.completeness.state !== "complete"
200
200
  ? {
@@ -45,16 +45,16 @@ export const membersHistory = async (services, chat, query) => {
45
45
  });
46
46
  };
47
47
  export const membersAudit = async (services, chat, query, options) => {
48
- const minScore = scoreFloor(listQuery(typedQuery(query), AUDIT_QUERY, "chats members audit"));
48
+ const minScore = scoreFloor(listQuery(typedQuery(query), AUDIT_QUERY, "chats members check"));
49
49
  const audit = await services.chats.audit(chat, { ...options, minScore });
50
50
  return { audit, body: auditAnswer(audit) };
51
51
  };
52
52
  /** The source takes a count but no offset, so page n reads n pages and shows the last. */
53
53
  export const inviteLinks = async (services, chat, query, { limit, page }) => {
54
- const terms = nativeTerms(listQuery(typedQuery(query), LINKS_QUERY, "chats link list"), "chats link list");
54
+ const terms = nativeTerms(listQuery(typedQuery(query), LINKS_QUERY, "chats links list"), "chats links list");
55
55
  const revoked = terms.get("revoked")?.value.toLowerCase() === "true";
56
56
  const found = await services.admin.links(chat, { limit: limit * page, revoked });
57
- return listOf("chats link list", {
57
+ return listOf("chats links list", {
58
58
  items: found.items.slice((page - 1) * limit).map(inviteLinkAnswer),
59
59
  limit,
60
60
  has_more: found.hasMore,
@@ -121,22 +121,22 @@ export const chatsCommand = (messenger) => {
121
121
  inspect: "https://example.com/invite/book-club",
122
122
  join: "https://example.com/invite/book-club",
123
123
  leave: '"Book club"',
124
- "link create": '"Book club"',
125
- "link list": '"Book club"',
126
- "link reset": '"Book club"',
127
- "link revoke": '"Book club" https://example.com/invite/book-club',
128
- "link show": '"Book club"',
124
+ "links create": '"Book club"',
125
+ "links list": '"Book club"',
126
+ "links reset": '"Book club"',
127
+ "links remove": '"Book club" https://example.com/invite/book-club',
128
+ "links show": '"Book club"',
129
129
  list: "--limit 10",
130
130
  "mark-read": '"Book club" --until-id 101',
131
131
  "members add": '"Book club" "Alex Example"',
132
- "members audit": '"Book club"',
132
+ "members check": '"Book club"',
133
133
  "members fetch": '"Book club"',
134
134
  "members history": '"Book club"',
135
135
  "members list": '"Book club"',
136
136
  "members remove": '"Book club" "Alex Example"',
137
137
  moderate: '"Book club" --dry-run',
138
- "requests accept": '"Book club" "Alex Example"',
139
- "requests decline": '"Book club" "Alex Example"',
138
+ "requests confirm": '"Book club" "Alex Example"',
139
+ "requests reject": '"Book club" "Alex Example"',
140
140
  "requests list": '"Book club"',
141
141
  "rules set": '"Book club" links report',
142
142
  "rules show": '"Book club"',
@@ -23,7 +23,7 @@ export const membersCommand = (messenger) => {
23
23
  const body = await context.withServices((services) => membersList(services, chat, context.settings));
24
24
  renderAnswer(this, context, body, lines(({ id, name, username, role }) => [id, name ?? "", username ? `@${username}` : "", role ?? ""].join(" ")));
25
25
  }));
26
- members.addCommand(withQuery(annotate(annotate(new Command("audit"), { effects: REMOTE_READ, offline: false })
26
+ members.addCommand(withQuery(annotate(annotate(new Command("check"), { effects: REMOTE_READ, offline: false })
27
27
  .summary("members that look like bots, with reasons")
28
28
  .description("members that look like bots, each with its reasons — read from the member list and the local store; " +
29
29
  "never one request per person, and it removes nobody")
@@ -69,7 +69,7 @@ export const contactsCommand = (messenger) => {
69
69
  }));
70
70
  renderAnswer(this, context, itemOf(contactAnswer(account, card)));
71
71
  }));
72
- annotate(contacts.command("profile"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
72
+ annotate(contacts.command("info"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
73
73
  .summary("a person's profile from the messenger")
74
74
  .description("everything the messenger says about one person — handles, flags, last seen, when they registered — and " +
75
75
  "how many of their messages the store holds in each chat you share, the first and the last, and the " +
@@ -173,7 +173,10 @@ export const contactsCommand = (messenger) => {
173
173
  meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
174
174
  }));
175
175
  });
176
- annotate(contacts.command("timeline"), { effects: REMOTE_READ, output: { entity: "contact_activity" } })
176
+ annotate(contacts.command("events"), {
177
+ effects: REMOTE_READ,
178
+ output: { entity: "contact_activity" },
179
+ })
177
180
  .summary("one person's activity across their linked identities")
178
181
  .description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
179
182
  "mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
@@ -200,7 +203,7 @@ export const contactsCommand = (messenger) => {
200
203
  ...(cursor === undefined ? {} : { cursor }),
201
204
  }));
202
205
  const { items, hasMore, limits, pending } = found;
203
- renderAnswer(this, context, listOf("contacts timeline", {
206
+ renderAnswer(this, context, listOf("contacts events", {
204
207
  items: items.map(snakeKeys),
205
208
  limit: limits.items,
206
209
  has_more: hasMore,
@@ -292,8 +295,8 @@ export const contactsCommand = (messenger) => {
292
295
  contacts.addCommand(command);
293
296
  const examples = {
294
297
  add: '"Alex Example"',
295
- "alias rm": '"Alex Example"',
296
- "alias set": '"Alex Example" "Reading partner"',
298
+ "aliases remove": '"Alex Example"',
299
+ "aliases set": '"Alex Example" "Reading partner"',
297
300
  block: '"Alex Example"',
298
301
  check: '"Alex Example"',
299
302
  context: '"Alex Example" --limit 10',
@@ -306,12 +309,12 @@ export const contactsCommand = (messenger) => {
306
309
  "notes list": '"Alex Example"',
307
310
  "notes remove": '"Alex Example" 1',
308
311
  "notes show": '"Alex Example" 1',
309
- profile: '"Alex Example"',
312
+ info: '"Alex Example"',
310
313
  remove: '"Alex Example"',
311
314
  rename: '"Alex Example" Alex Example',
312
315
  show: '"Alex Example"',
313
316
  sync: "",
314
- timeline: '"Alex Example" --limit 10',
317
+ events: '"Alex Example" --limit 10',
315
318
  unblock: '"Alex Example"',
316
319
  unlink: '"Alex Example"',
317
320
  };
@@ -1,5 +1,5 @@
1
1
  import type { Command } from "commander";
2
- import type { AdminRight, GroupSettingKey, Id, Message, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
2
+ import type { AdminRight, GroupSettingKey, Id, Message, Page, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
3
3
  import type { MessengerAdapter } from "../../ports/messenger.js";
4
4
  import type { EventSink, Warn } from "../../runs/events.js";
5
5
  import { type Asker, type SendGuard } from "../../sends/guard.js";
@@ -18,6 +18,11 @@ import type { GlobalFlags, ResolveOptions, Settings } from "../settings.js";
18
18
  */
19
19
  export interface StoredMessages {
20
20
  message(chatId: Id, messageId: Id): Promise<Message | undefined>;
21
+ /** Stored messages in one provider thread, oldest first within the newest page. No remote fetch. */
22
+ threadMessages?(chatId: Id, threadId: Id, window: {
23
+ limit: number;
24
+ before?: Id;
25
+ }): Promise<Page<Message>>;
21
26
  /** The newest message from someone else in the chat, older than `before` when it is given. */
22
27
  newestIncoming(chatId: Id, before?: Id): Promise<Message | undefined>;
23
28
  /** The newest message in the chat sent at `at` or earlier; without `at`, the oldest one held. */
@@ -148,20 +153,22 @@ export interface Messenger extends ServiceMessenger {
148
153
  stickerFileBytes?: number;
149
154
  /** Whether shared folders can be joined; yes when unset. */
150
155
  folderJoin?: boolean;
151
- /** Whether another invite link can be made — `chats link create`; yes when unset. */
156
+ /** Whether another invite link can be made — `chats links create`; yes when unset. */
152
157
  inviteLinkCreate?: boolean;
153
158
  /** Whether invite links can be listed; yes when unset. */
154
159
  inviteLinkList?: boolean;
155
160
  /** Whether one invite link can be revoked; yes when unset. */
156
161
  inviteLinkRevoke?: boolean;
157
- /** Whether one invite link can be changed — `chats link update`; no when unset. */
162
+ /** Whether one invite link can be changed — `chats links update`; no when unset. */
158
163
  inviteLinkUpdate?: boolean;
159
- /** Whether every pending join request is answered at once — `chats requests accept --all`; yes when unset. */
164
+ /** Whether every pending join request is answered at once — `chats requests confirm --all`; yes when unset. */
160
165
  joinRequestsAll?: boolean;
161
166
  /** Whether `--all` can take only the requests made by one invite link — `--link`; yes when unset. */
162
167
  joinRequestsByLink?: boolean;
163
168
  /** Whether a channel post can be commented on — `messages send --comment-to`; yes when unset. */
164
169
  channelComments?: boolean;
170
+ /** Whether comments can be read independently of sending; inherits channelComments when unset. */
171
+ readsComments?: boolean;
165
172
  /** Whether people added can be shown the messages from before they came — `members add --history`; yes when unset. */
166
173
  addsWithHistory?: boolean;
167
174
  /** The admin rights this messenger has, as `admins add --can` offers them; every one when unset. */
@@ -40,6 +40,12 @@ export const storedMessages = (provider, store, account) => {
40
40
  const at = key();
41
41
  return at && (await store()).messages.message(at, messageId, { chatId });
42
42
  },
43
+ threadMessages: async (chatId, threadId, window) => {
44
+ const owner = key();
45
+ return owner
46
+ ? (await store()).messages.messages(owner, chatId, { ...window, threadId })
47
+ : { items: [], hasMore: false };
48
+ },
43
49
  newestIncoming: async (chatId, before) => {
44
50
  const at = key();
45
51
  if (!at)
@@ -2,13 +2,14 @@ import { annotate } from "@wirecat/cli-base/commands";
2
2
  import { commandAt } from "../steps.js";
3
3
  export const conversationExamples = (root, tool) => {
4
4
  const examples = {
5
- "batches next": '--chat "Book club"',
5
+ "batches show": '--chat "Book club"',
6
6
  "batches status": '--chat "Book club"',
7
- build: '--chat "Book club"',
7
+ "links rebuild": '--chat "Book club"',
8
8
  "consents list": "",
9
- "consents revoke": '--chat "Book club"',
10
- "embed clear": '--chat "Book club"',
11
- "embed status": '--chat "Book club"',
9
+ "consents remove": '--chat "Book club"',
10
+ embed: '--chat "Book club"',
11
+ "embeddings clear": '--chat "Book club"',
12
+ "embeddings status": '--chat "Book club"',
12
13
  "links add": '--batch "$batch"',
13
14
  "links clear": '--chat "Book club"',
14
15
  list: '"Book club"',
@@ -21,5 +22,5 @@ export const conversationExamples = (root, tool) => {
21
22
  if (command)
22
23
  annotate(command, { examples: [`${tool} personal conversations ${path} ${args}`.trimEnd()] });
23
24
  }
24
- commandAt(root, "links add")?.addHelpText("after", "\nSet batch to the id from conversations batches next; supply the JSON answer to that batch on stdin.\n");
25
+ commandAt(root, "links add")?.addHelpText("after", "\nSet batch to the id from conversations batches show; supply the JSON answer to that batch on stdin.\n");
25
26
  };
@@ -1,9 +1,5 @@
1
1
  import { Command } from "commander";
2
2
  import { type Messenger } from "./context.js";
3
- /**
4
- * `conversations`: the threads inside a group chat, found by rules over the stored messages — replies,
5
- * mentions, one sender's messages in a row. From the store alone; nothing is built until asked (phase 3).
6
- */
7
3
  export declare const conversationsCommand: (messenger: Messenger) => Command;
8
4
  /** `messages links`: why a message sits where it does in its conversation. */
9
5
  export declare const linksCommand: (messenger: Messenger) => Command;