@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
@@ -1,7 +1,8 @@
1
1
  import { join } from "node:path";
2
2
  import { CliError } from "@wirecat/cli-base";
3
3
  import { annotate } from "@wirecat/cli-base/commands";
4
- import { Command, Option } from "commander";
4
+ import { defineQuery } from "@wirecat/cli-base/query";
5
+ import { Command } from "commander";
5
6
  import { isTextModelInstalled, textModelsDirectory } from "../../embeddings/embed.js";
6
7
  import { embeddingKeys } from "../../embeddings/keys.js";
7
8
  import { DEFAULT_TEXT_MODEL, TEXT_MODELS } from "../../embeddings/models.js";
@@ -15,10 +16,19 @@ import { readSecret } from "../../terminal/prompt.js";
15
16
  import { baseContext, environmentOf } from "../context.js";
16
17
  import { FETCH, grantsAuthority, LOCAL_READ, LOCAL_WRITE, reachesOtherService } from "../effects.js";
17
18
  import { itemOf, listOf, renderAnswer } from "../envelope.js";
18
- import { listQuery, typedQuery, unfilteredQuery, withQuery } from "../list-query.js";
19
+ import { listQuery, nativeTerms, termField, typedQuery, unfilteredQuery, withQuery } from "../list-query.js";
19
20
  import { nextStep } from "../steps.js";
20
21
  /** A model list is the catalog this version ships. */
21
22
  const MODELS_QUERY = unfilteredQuery("model", modelEntity);
23
+ const ROOT_MODELS_QUERY = defineQuery({
24
+ entity: "model",
25
+ backend: "native",
26
+ fields: [
27
+ termField("kind", "enum", "kind:audio", { values: ["audio", "text"] }),
28
+ termField("id", "kw", "id:e5-small"),
29
+ termField("downloaded", "bool", "downloaded:true"),
30
+ ],
31
+ });
22
32
  /** Every model this version knows: a short, fixed list, never paged. */
23
33
  const everyModel = (command, items, next = []) => listOf(command, { items, limit: Math.max(1, items.length), has_more: false, next });
24
34
  const audioItem = (model, directory, chosen) => ({
@@ -56,97 +66,47 @@ const imageItem = (model, directory) => ({
56
66
  const downloaded = (body) => "item" in body ? `${body.item.id}: downloaded and works, in ${body.item.path}\n` : "";
57
67
  const modelLine = (item, width) => `${item.default ? "*" : " "} ${item.id.padEnd(width)} ${megabytes(item.size_bytes).padStart(7)} ` +
58
68
  (item.downloaded ? "downloaded" : "—".padEnd(10));
59
- export const modelsCommand = (messenger) => {
60
- const tool = `${messenger.app.command} personal`;
61
- const models = new Command("models").description("models that run on this machine");
62
- const audio = models.command("audio").description("speech models for transcribing voice messages");
63
- withQuery(annotate(audio.command("list"), { effects: LOCAL_READ, examples: [`${tool} models audio list`] }), MODELS_QUERY)
64
- .summary("speech models and their download status")
65
- .description("the speech models, most suitable first, which are downloaded, and which one is the default")
66
- .action(async function (words) {
67
- const context = baseContext(this, messenger.resolveSettings);
68
- await context.run(async () => {
69
- listQuery(typedQuery(words), MODELS_QUERY, "models audio list");
70
- const directory = modelsDirectory(context.env);
71
- const chosen = choose(messenger, context.settings, {}, context.env).model.id;
72
- const items = orderedModels(messenger.speechModels).map((model) => audioItem(model, directory, chosen));
73
- renderAnswer(this, context, everyModel("models audio list", items), () => items.map((item) => `${modelLine(item, 14)} ${item.languages}\n`).join(""));
74
- if (context.format === "pretty")
75
- context.renderer.note(`kept in ${directory}`);
76
- });
77
- });
78
- reachesOtherService(annotate(audio.command("download"), {
79
- effects: FETCH,
80
- output: { entity: "model" },
81
- examples: [`${tool} models audio download parakeet-v3`],
82
- }))
83
- .argument("<model>", "a model id from `models audio list`")
84
- .description("download a speech model once, checked against the sha256 this version expects")
85
- .action(async function (id) {
86
- const context = baseContext(this, messenger.resolveSettings);
87
- await context.run(async () => {
88
- const model = speechModel(id);
89
- const directory = modelsDirectory(context.env);
90
- if (!isInstalled(model, directory)) {
91
- context.renderer.note(`${model.id}: ${megabytes(installedBytes(model) + VAD.bytes)} from Hugging Face and GitHub`);
92
- await install(model, directory, { progress: (line) => context.renderer.note(line) });
93
- }
94
- // Loading is the test — the files, the WebAssembly engine and the memory it needs — on a
95
- // second of silence, since no recording ships with the package.
96
- context.renderer.note(`${model.id}: loading it once to check it works`);
97
- const { openRecognizer, SAMPLE_RATE } = await import("../../speech/recognize.js");
98
- const recognizer = openRecognizer(model, modelPath(directory, model), vadPath(directory));
99
- try {
100
- recognizer.recognize(new Float32Array(SAMPLE_RATE));
101
- }
102
- finally {
103
- recognizer.free();
104
- }
105
- const chosen = choose(messenger, context.settings, {}, context.env).model.id;
106
- renderAnswer(this, context, itemOf(audioItem(model, directory, chosen)), downloaded);
107
- }, { unbounded: true });
108
- });
109
- const text = models.command("text").description("embedding models for searching conversations by meaning");
110
- withQuery(annotate(text.command("list"), { effects: LOCAL_READ, examples: [`${tool} models text list`] }), MODELS_QUERY)
111
- .summary("embedding models and their download status")
112
- .description("the embedding models, most suitable first, which are downloaded, and which one is the default")
113
- .action(async function (words) {
114
- const context = baseContext(this, messenger.resolveSettings);
115
- await context.run(async () => {
116
- listQuery(typedQuery(words), MODELS_QUERY, "models text list");
117
- const directory = textModelsDirectory(context.env);
118
- const items = TEXT_MODELS.map((model) => textItem(model, directory));
119
- const missing = items.find((item) => item.default && !item.downloaded);
120
- renderAnswer(this, context, everyModel("models text list", items, missing ? [nextStep("models.download", { args: { model: missing.id } })] : []), () => items.map((item) => `${modelLine(item, 16)} ${item.licence}\n`).join(""));
121
- if (context.format === "pretty")
122
- context.renderer.note(`kept in ${directory}`);
69
+ const downloadAudioModel = async (command, messenger, id) => {
70
+ const context = baseContext(command, messenger.resolveSettings);
71
+ await context.run(async () => {
72
+ const model = speechModel(id);
73
+ const directory = modelsDirectory(context.env);
74
+ if (!isInstalled(model, directory)) {
75
+ context.renderer.note(`${model.id}: ${megabytes(installedBytes(model) + VAD.bytes)} from Hugging Face and GitHub`);
76
+ await install(model, directory, { progress: (line) => context.renderer.note(line) });
77
+ }
78
+ // Loading is the test — the files, the WebAssembly engine and the memory it needs — on a
79
+ // second of silence, since no recording ships with the package.
80
+ context.renderer.note(`${model.id}: loading it once to check it works`);
81
+ const { openRecognizer, SAMPLE_RATE } = await import("../../speech/recognize.js");
82
+ const recognizer = openRecognizer(model, modelPath(directory, model), vadPath(directory));
83
+ try {
84
+ recognizer.recognize(new Float32Array(SAMPLE_RATE));
85
+ }
86
+ finally {
87
+ recognizer.free();
88
+ }
89
+ const chosen = choose(messenger, context.settings, {}, context.env).model.id;
90
+ renderAnswer(command, context, itemOf(audioItem(model, directory, chosen)), downloaded);
91
+ }, { unbounded: true });
92
+ };
93
+ const downloadEmbeddingModel = async (command, messenger, id, options) => {
94
+ const context = baseContext(command, messenger.resolveSettings);
95
+ await context.run(async () => {
96
+ const { model, directory } = await downloadTextModel(id, {
97
+ env: context.env,
98
+ acceptTerms: options.acceptTerms === true,
99
+ progress: (line) => context.renderer.note(line),
123
100
  });
124
- });
125
- reachesOtherService(annotate(text.command("download"), {
126
- effects: FETCH,
127
- output: { entity: "model" },
128
- examples: [`${tool} models text download ${DEFAULT_TEXT_MODEL}`],
129
- }))
130
- .argument("<model>", "a model id from `models text list`")
131
- .addOption(new Option("--accept-terms", "accept the model's licence terms, for a model that has its own"))
132
- .summary("download and verify an embedding model")
133
- .description("download an embedding model once, checked against the sha256 this version expects")
134
- .action(async function (id, options) {
135
- const context = baseContext(this, messenger.resolveSettings);
136
- await context.run(async () => {
137
- const { model, directory } = await downloadTextModel(id, {
138
- env: context.env,
139
- acceptTerms: options.acceptTerms === true,
140
- progress: (line) => context.renderer.note(line),
141
- });
142
- renderAnswer(this, context, itemOf(textItem(model, directory)), downloaded);
143
- }, { unbounded: true });
144
- });
145
- const key = text.command("key").description("API keys for embedding and analysis providers");
101
+ renderAnswer(command, context, itemOf(textItem(model, directory)), downloaded);
102
+ }, { unbounded: true });
103
+ };
104
+ const modelKeysCommand = (messenger, tool) => {
105
+ const key = new Command("keys").description("API keys for embedding and analysis providers");
146
106
  grantsAuthority(annotate(key.command("set"), {
147
107
  effects: LOCAL_WRITE,
148
108
  output: { entity: "model_key", view: "stored" },
149
- examples: [`${tool} models text key set openai`],
109
+ examples: [`${tool} models keys set openai`],
150
110
  }))
151
111
  .argument("<provider>", "openai, anthropic, or the host of a --base-url server that wants a key")
152
112
  .description("store a key, typed at a hidden prompt or piped on stdin — never as an argument")
@@ -164,7 +124,7 @@ export const modelsCommand = (messenger) => {
164
124
  grantsAuthority(annotate(key.command("remove"), {
165
125
  effects: LOCAL_WRITE,
166
126
  output: { entity: "model_key", view: "removed" },
167
- examples: [`${tool} models text key remove openai`],
127
+ examples: [`${tool} models keys remove openai`],
168
128
  }))
169
129
  .argument("<provider>", "openai, anthropic, or a server's host")
170
130
  .description("forget a stored key")
@@ -174,6 +134,57 @@ export const modelsCommand = (messenger) => {
174
134
  renderAnswer(this, context, itemOf({ provider, removed: embeddingKeys(messenger.app, context.env, context.keyring).remove(provider) }));
175
135
  });
176
136
  });
137
+ return key;
138
+ };
139
+ export const modelsCommand = (messenger) => {
140
+ const tool = `${messenger.app.command} personal`;
141
+ const models = new Command("models").description("models that run on this machine");
142
+ models.addCommand(modelKeysCommand(messenger, tool));
143
+ withQuery(annotate(models.command("list"), {
144
+ effects: LOCAL_READ,
145
+ examples: [`${tool} models list`, `${tool} models list kind:audio`],
146
+ }), ROOT_MODELS_QUERY)
147
+ .description("speech and embedding models, their download status and the default of each kind")
148
+ .action(async function (words) {
149
+ const terms = nativeTerms(listQuery(typedQuery(words), ROOT_MODELS_QUERY, "models list"), "models list");
150
+ const context = baseContext(this, messenger.resolveSettings);
151
+ await context.run(async () => {
152
+ const kinds = terms.get("kind")?.value;
153
+ const id = terms.get("id")?.value;
154
+ const kept = terms.get("downloaded")?.value;
155
+ const audioModels = kinds === undefined || kinds === "audio"
156
+ ? orderedModels(messenger.speechModels).filter((model) => id === undefined || model.id === id)
157
+ : [];
158
+ const textModels = kinds === undefined || kinds === "text"
159
+ ? TEXT_MODELS.filter((model) => id === undefined || model.id === id)
160
+ : [];
161
+ const chosen = audioModels.length > 0 ? choose(messenger, context.settings, {}, context.env).model.id : "";
162
+ const catalogue = [
163
+ ...audioModels.map((model) => audioItem(model, modelsDirectory(context.env), chosen)),
164
+ ...textModels.map((model) => textItem(model, textModelsDirectory(context.env))),
165
+ ];
166
+ const items = catalogue.filter((item) => kept === undefined || item.downloaded === (kept === "true"));
167
+ const next = items
168
+ .filter((item) => item.default && !item.downloaded)
169
+ .map((item) => nextStep("models.download", { args: { model: item.id } }));
170
+ renderAnswer(this, context, everyModel("models list", items, next), () => items.map((item) => `${item.kind.padEnd(5)} ${modelLine(item, 18)} ${item.languages}\n`).join(""));
171
+ });
172
+ });
173
+ reachesOtherService(annotate(models.command("download"), {
174
+ effects: FETCH,
175
+ output: { entity: "model" },
176
+ examples: [`${tool} models download parakeet-v3`, `${tool} models download ${DEFAULT_TEXT_MODEL}`],
177
+ }))
178
+ .description("download and verify one speech or embedding model")
179
+ .argument("<model>", "a model id from models list")
180
+ .option("--accept-terms", "accept the model's licence terms, for a model that has its own")
181
+ .action(async function (id, options) {
182
+ if (orderedModels().some((model) => model.id === id))
183
+ return downloadAudioModel(this, messenger, id);
184
+ if (TEXT_MODELS.some((model) => model.id === id))
185
+ return downloadEmbeddingModel(this, messenger, id, options);
186
+ throw new CliError("validation_error", "unknown model; choose an id from models list");
187
+ });
177
188
  return models;
178
189
  };
179
190
  /** `images models`: registered with picture search, the only thing these models serve. */
@@ -13,22 +13,22 @@ import { readAll } from "./stdin.js";
13
13
  const NOTES_QUERY = unfilteredQuery("contact_note", contactNote);
14
14
  const everyNote = (command, items) => listOf(command, { items, limit: Math.max(1, items.length), has_more: false });
15
15
  export const privatePeopleCommands = (messenger) => {
16
- const alias = new Command("alias").description("a private local display name in the selected account");
16
+ const alias = new Command("aliases").description("a private local display name in the selected account");
17
17
  annotate(alias.command("set"), { effects: LOCAL_WRITE, output: { entity: "contact_alias" } })
18
18
  .description("your own name for a person in this account; kept on this machine, never sent")
19
19
  .argument("<person>", "the person's id, reference or known name")
20
20
  .argument("<alias>", "the private name to use for this person")
21
21
  .action(async function (person, value) {
22
22
  const context = messengerContext(this, messenger);
23
- refuseLocalWrite(context, messenger.app.command, "contacts.alias.set");
23
+ refuseLocalWrite(context, messenger.app.command, "contacts.aliases.set");
24
24
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, value)))));
25
25
  });
26
- annotate(alias.command("rm"), { effects: LOCAL_WRITE, output: { entity: "contact_alias" } })
26
+ annotate(alias.command("remove"), { effects: LOCAL_WRITE, output: { entity: "contact_alias" } })
27
27
  .description("drop your local name for a person; their own name shows again")
28
28
  .argument("<person>", "the person's id, reference or known name")
29
29
  .action(async function (person) {
30
30
  const context = messengerContext(this, messenger);
31
- refuseLocalWrite(context, messenger.app.command, "contacts.alias.rm");
31
+ refuseLocalWrite(context, messenger.app.command, "contacts.aliases.remove");
32
32
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, null)))));
33
33
  });
34
34
  const notes = new Command("notes").description("your private notes on a stored contact, the same in every account that sees them");
@@ -57,8 +57,7 @@ export const privatePeopleCommands = (messenger) => {
57
57
  ? "add a private note on a person from stdin or --text-file; never sent"
58
58
  : "replace a note's text; --revision must match the one you read")
59
59
  .argument("<person>", "the person's id, reference or known name")
60
- .addOption(ownerOnlyOption("--text-file <path>", "read note text from a file; omitted or - reads stdin").conflicts("file"))
61
- .addOption(ownerOnlyOption("--file <path>", "compatibility alias for --text-file").hideHelp().conflicts("textFile"));
60
+ .addOption(ownerOnlyOption("--text-file <path>", "read note text from a file; omitted or - reads stdin"));
62
61
  if (name === "edit")
63
62
  command
64
63
  .argument("<id>", "the note id from contacts notes list")
@@ -65,7 +65,6 @@ export const addReplyEditors = (replies, messenger) => {
65
65
  option.conflicts("model");
66
66
  edit.addOption(option);
67
67
  }
68
- edit.addOption(new Option("--model <mode>", "compatibility alias for --template-mode").hideHelp().conflicts("templateMode"));
69
68
  // Commander gives negated-only flags a true default; omission must preserve the saved field.
70
69
  edit.action(function (id) {
71
70
  const options = this.opts();
@@ -1,9 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { Command, Option } from "commander";
3
+ import { Command } from "commander";
4
4
  import { parseDuration } from "../../domain/duration.js";
5
5
  import { renderMessages } from "../../render/messages.js";
6
- import { CHAT_KINDS } from "../../services/chats.js";
7
6
  import { kindsOf, REVIEW_DAYS, reviewStart, UNANSWERED_HOURS } from "../../services/inbox.js";
8
7
  import { modelWith } from "../../speech/hearing.js";
9
8
  import { REMOTE_READ } from "../effects.js";
@@ -34,7 +33,6 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
34
33
  .description("every message, yours too, in chats that changed since a point — for reviewing who owes what")
35
34
  .addHelpText("after", queryHelp(REVIEW_QUERY))
36
35
  .argument("[query...]", `where the last review ended and which chat: date>2h, chat:"Book club"; ${REVIEW_DAYS} days ago if no date`)
37
- .addOption(new Option("--kind <kinds>", `compatibility spelling for kind:; ${CHAT_KINDS.join(", ")}`).hideHelp())
38
36
  .option("--unanswered [duration]", `only questions to you or a group's admins that nobody answered, asked at least this long ago — 4h, 1d; ${UNANSWERED_HOURS}h if not given`)
39
37
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
40
38
  .option(...TRANSCRIBE_OPTION)
@@ -81,6 +81,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
81
81
  joinRequestsAll?: boolean;
82
82
  joinRequestsByLink?: boolean;
83
83
  channelComments?: boolean;
84
+ readsComments?: boolean;
84
85
  addsWithHistory?: boolean;
85
86
  adminRights?: readonly import("../../domain/models.js").AdminRight[];
86
87
  pickAdminRights?: boolean;
@@ -93,7 +93,7 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
93
93
  "contacts evidence": '101 --selection "$selection" --component messages',
94
94
  "contacts responses": 'chat:"Project room"',
95
95
  "contacts top": 'chat:"Book club" --limit 10',
96
- "messages counters refresh": '--chat "Book club" --dry-run',
96
+ "messages counters sync": '--chat "Book club" --dry-run',
97
97
  "messages counters show": '--chat "Book club" --limit 10',
98
98
  "messages discussion": 'chat:"Book club"',
99
99
  "messages evidence": '"$message" --selection "$selection" --component reactions',
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
3
3
  import type { Messenger } from "./context.js";
4
4
  /** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
5
5
  export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
6
+ error?: undefined;
6
7
  path: string;
7
8
  exists: boolean;
8
- error?: undefined;
9
9
  } | {
10
10
  path: string;
11
11
  exists: boolean;
@@ -22,7 +22,7 @@ export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
22
22
  export declare const storeMaintenanceCommands: (messenger: Messenger) => Command[];
23
23
  /**
24
24
  * Stems what is left, after rebuilding stems made by other stemmer choices than the store's setting —
25
- * only here and in `store reindex`, never in a search's quick fill.
25
+ * only here and in `store rebuild`, never in a search's quick fill.
26
26
  */
27
27
  export declare const buildStems: (database: CacheDatabase, note: (text: string) => void, { force }?: {
28
28
  force?: boolean | undefined;
@@ -237,7 +237,7 @@ const checkCommand = (messenger) => annotate(new Command("check"), { output: { e
237
237
  renderer.note(`${orphans} pointers name a row that is gone or a type the store does not know — see orphan_pointers`);
238
238
  const older = answer.conversations.filter((chat) => chat.provider === messenger.provider && !chat.current).length;
239
239
  if (older > 0) {
240
- renderer.note(`${older} chats' conversations were built by older rules — \`${command} conversations build --chat <chat>\``);
240
+ renderer.note(`${older} chats' conversations were built by older rules — \`${command} conversations links rebuild --chat <chat>\``);
241
241
  }
242
242
  });
243
243
  const inspect = (path) => reading(path, (database) => {
@@ -407,7 +407,7 @@ const buildWordIndex = (database, note) => {
407
407
  };
408
408
  /**
409
409
  * Stems what is left, after rebuilding stems made by other stemmer choices than the store's setting —
410
- * only here and in `store reindex`, never in a search's quick fill.
410
+ * only here and in `store rebuild`, never in a search's quick fill.
411
411
  */
412
412
  export const buildStems = (database, note, { force = false } = {}) => {
413
413
  const before = stemsState(database);
@@ -422,7 +422,7 @@ export const buildStems = (database, note, { force = false } = {}) => {
422
422
  const { stemmed, drained } = fillStems(database, { onBatch: (step, done) => note(`${done} ${step}`) });
423
423
  return { stemmed: stemmed + drained };
424
424
  };
425
- /** Indexes the notes written since — all of them after `store reindex` queued them again. */
425
+ /** Indexes the notes written since — all of them after `store rebuild` queued them again. */
426
426
  const buildNoteIndex = (database, note, { reset = false } = {}) => {
427
427
  if (reset)
428
428
  resetNoteIndex(database);
@@ -431,7 +431,7 @@ const buildNoteIndex = (database, note, { reset = false } = {}) => {
431
431
  note(`indexing ${pending} notes`);
432
432
  return { notesIndexed: drainNoteIndex(database, stemmerCache()) };
433
433
  };
434
- const reindexCommand = (messenger) => destroysNothing(annotate(new Command("reindex"), { output: { entity: "store_reindex" }, effects: LOCAL_WRITE }))
434
+ const reindexCommand = (messenger) => destroysNothing(annotate(new Command("rebuild"), { output: { entity: "store_reindex" }, effects: LOCAL_WRITE }))
435
435
  .summary("rebuild the local archive's search indexes")
436
436
  .description("rebuild the word index, its typo vocabulary, the stems, the files' word index, the notes' indexes and who took part in what, from what is stored; loses nothing")
437
437
  .action(async function () {
@@ -534,6 +534,11 @@ const fill = async (store, account, batch) => {
534
534
  of(chatId).stars.push({ messageId, starred });
535
535
  for (const [chatId, { messages, votes, reactions, stars }] of byChat)
536
536
  await store.messages.saveMessages(account, chatId, messages, { via: "history", votes, reactions, stars });
537
+ const deletions = new Map();
538
+ for (const { chatId, messageId } of batch.deletions ?? [])
539
+ deletions.set(chatId, [...(deletions.get(chatId) ?? []), messageId]);
540
+ for (const [chatId, ids] of deletions)
541
+ await store.messages.markDeleted(account, ids, { chatId });
537
542
  const changes = new Map();
538
543
  for (const { chatId, change } of batch.events ?? [])
539
544
  changes.set(chatId, [...(changes.get(chatId) ?? []), change]);
@@ -32,10 +32,10 @@ export const tagsCommand = (messenger) => {
32
32
  const tags = new Command("tags")
33
33
  .summary("tags on stored messages and other records")
34
34
  .description("your own labels on chats, people, messages and tasks, kept in the local store and never sent; tag: in a search finds them");
35
- changesStore(annotate(tags.command("auto"), {
35
+ changesStore(annotate(tags.command("rebuild"), {
36
36
  output: { entity: "automatic_tags" },
37
37
  effects: FETCH,
38
- examples: [`${tool} tags auto --chat "Book club" --dry-run`],
38
+ examples: [`${tool} tags rebuild --chat "Book club" --dry-run`],
39
39
  }))
40
40
  .description("derive local group/channel tags from cached metadata using keyword rules")
41
41
  .option("--chat <chat>", "a stored group/channel; repeat to select several", (value, previous) => [...previous, value], [])
@@ -46,7 +46,7 @@ export const tagsCommand = (messenger) => {
46
46
  const context = messengerContext(this, messenger);
47
47
  const { chat, limit, syncFirst, dryRun } = this.opts();
48
48
  if (!dryRun)
49
- refuseLocalWrite(context, messenger.app.command, "tags.auto");
49
+ refuseLocalWrite(context, messenger.app.command, "tags.rebuild");
50
50
  const done = await context.withServices((services) => services.metadata.auto({ chats: chat, limit: Number(limit), refresh: syncFirst, dryRun }));
51
51
  renderAnswer(this, context, itemOf(snakeKeys(done)));
52
52
  });
@@ -68,13 +68,7 @@ export const tagsCommand = (messenger) => {
68
68
  effects: LOCAL_WRITE,
69
69
  examples: [`${tool} tags remove reading --chat "Book club"`],
70
70
  }).description("take tags off one chat, person, message or task"), messenger, "untag")
71
- .addOption(new Option("--claim <claim>", "remove only this ownership claim: manual or auto")
72
- .argParser(sourceOf)
73
- .conflicts("source"))
74
- .addOption(new Option("--source <manual|auto>", "compatibility alias for --claim")
75
- .argParser(sourceOf)
76
- .hideHelp()
77
- .conflicts("claim"))
71
+ .addOption(new Option("--claim <claim>", "remove only this ownership claim: manual or auto").argParser(sourceOf))
78
72
  .action(async function (given) {
79
73
  const context = messengerContext(this, messenger);
80
74
  refuseLocalWrite(context, messenger.app.command, "tags.remove");
@@ -185,17 +185,17 @@ export const tasksCommand = (messenger) => {
185
185
  else if (followUpOf && context.format === "pretty")
186
186
  context.renderer.note(`${task.key ?? task.id} follows up ${followUpOf.key} (${followUpOf.status}), which stays closed: linked follow-up-of`);
187
187
  });
188
- planOptions(destroysNothing(annotate(tasks.command("plan"), {
188
+ planOptions(destroysNothing(annotate(tasks.command("update"), {
189
189
  output: { entity: "task_card" },
190
190
  effects: LOCAL_WRITE,
191
- examples: [`${tool} personal tasks plan DOC-12 --title "Review draft"`],
191
+ examples: [`${tool} personal tasks update DOC-12 --title "Review draft"`],
192
192
  }))
193
193
  .summary("set a task's title, description, due date, next check, category and assignee")
194
194
  .description("set a task's title, description, due date, next check, category and assignee; nothing reaches the messenger")
195
195
  .argument("<task>", "the task's id or key, as tasks list shows it")
196
196
  .option("--category <name>", `${categoryHelp}; a change is kept in the task's history`), "send Alex the API token").action(async function (id) {
197
197
  const context = messengerContext(this, messenger);
198
- refuseLocalWrite(context, messenger.app.command, "tasks.plan");
198
+ refuseLocalWrite(context, messenger.app.command, "tasks.update");
199
199
  const plan = planFrom(this.opts());
200
200
  if (Object.keys(plan).length === 0)
201
201
  throw new CliError("validation_error", "give at least one of --title, --description, --due, --remind-time, --category");
@@ -18,7 +18,7 @@ export const transcribeSubcommand = (messages, messenger) => annotate(messages.c
18
18
  .argument("<chat>", messenger.chatArgument)
19
19
  .argument("<message>", "the id of a voice message")
20
20
  .option("--local", "use the model on this machine, never the messenger")
21
- .option("--model <model>", "which downloaded model; implies --local (`models audio list`)")
21
+ .option("--model <model>", "which downloaded model; implies --local (`models list`)")
22
22
  .action(async function (chat, messageId) {
23
23
  const context = messengerContext(this, messenger);
24
24
  const choice = {
@@ -7,6 +7,8 @@ export declare const MOVED_TO_MEMO: Readonly<Record<string, {
7
7
  what: string;
8
8
  memo: string;
9
9
  }>>;
10
+ /** Retired spellings are guidance only; none is mounted as a command alias. */
11
+ export declare const RENAMED_COMMANDS: Readonly<Record<string, string>>;
10
12
  export declare const movedPointer: (path: string) => string | undefined;
11
13
  /** The longest moved path the words start with, unless the program still has it. */
12
14
  export declare const movedPath: (words: readonly string[], has: (path: string) => boolean) => string | undefined;
package/dist/cli/moved.js CHANGED
@@ -6,12 +6,50 @@
6
6
  export const MOVED_TO_MEMO = {
7
7
  "search mail": { what: "mail is searched by the separate memo CLI", memo: "memo search mail …" },
8
8
  };
9
+ /** Retired spellings are guidance only; none is mounted as a command alias. */
10
+ export const RENAMED_COMMANDS = {
11
+ "contacts alias rm": "contacts aliases remove",
12
+ "contacts alias": "contacts aliases",
13
+ "contacts profile": "contacts info",
14
+ "contacts timeline": "contacts events",
15
+ "chats link revoke": "chats links remove",
16
+ "chats link": "chats links",
17
+ "chats members audit": "chats members check",
18
+ "chats requests accept": "chats requests confirm",
19
+ "chats requests decline": "chats requests reject",
20
+ "metadata get": "metadata show",
21
+ "metadata refresh": "metadata sync",
22
+ "tasks plan": "tasks update",
23
+ "tags auto": "tags rebuild",
24
+ "attachments text set": "attachments update",
25
+ "conversations build": "conversations links rebuild",
26
+ "conversations embed status": "conversations embeddings status",
27
+ "conversations embed clear": "conversations embeddings clear",
28
+ "conversations batches next": "conversations batches show",
29
+ "conversations consents revoke": "conversations consents remove",
30
+ "doctor report create": "doctor reports create",
31
+ "doctor report": "doctor reports show",
32
+ "mcp setup": "mcp add",
33
+ "mcp doctor": "mcp check",
34
+ "commands schema": "commands info",
35
+ "store gaps plan": "store gaps show",
36
+ "store reindex": "store rebuild",
37
+ "stats messages counters refresh": "stats messages counters sync",
38
+ "models text key": "models keys",
39
+ "models audio list": "models list kind:audio",
40
+ "models text list": "models list kind:text",
41
+ "models audio download": "models download",
42
+ "models text download": "models download",
43
+ };
9
44
  export const movedPointer = (path) => {
45
+ const renamed = RENAMED_COMMANDS[path];
46
+ if (renamed)
47
+ return `use \`${renamed}\``;
10
48
  const moved = MOVED_TO_MEMO[path];
11
49
  return moved && `${moved.what}: install @wirecat/cli-memo, then \`${moved.memo}\``;
12
50
  };
13
51
  /** The longest moved path the words start with, unless the program still has it. */
14
- export const movedPath = (words, has) => Object.keys(MOVED_TO_MEMO)
52
+ export const movedPath = (words, has) => [...Object.keys(MOVED_TO_MEMO), ...Object.keys(RENAMED_COMMANDS)]
15
53
  .filter((path) => path.split(" ").every((word, at) => words[at] === word))
16
54
  .sort((a, b) => b.length - a.length)
17
55
  .find((path) => !has(path));
@@ -1,3 +1,4 @@
1
+ import { canonicalPermissionKey } from "../sends/permission-aliases.js";
1
2
  import { CEILING_KEY, keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
2
3
  const pathOf = (command) => {
3
4
  const words = [];
@@ -19,6 +20,7 @@ const everyCommand = (command) => [command, ...command.commands.flatMap(everyCom
19
20
  export const knownPermissionKeys = (command, keyOf = (one) => keyForCommand(pathOf(one)), extra = []) => {
20
21
  const known = new Set();
21
22
  const add = (key) => {
23
+ key = canonicalPermissionKey(key);
22
24
  for (let parts = key.split("."); parts.length > 0; parts = parts.slice(0, -1))
23
25
  known.add(parts.join("."));
24
26
  };
@@ -60,7 +60,7 @@ const SHARED_PROFILE_ENTRIES = {
60
60
  sends_per_hour: v.optional(count),
61
61
  requests_per_minute: v.optional(countOrZero),
62
62
  transcribe_with: v.optional(v.picklist(["auto", "messenger", "local"], plain("has to be auto, messenger or local"))),
63
- speech_model: v.optional(v.string(plain("has to be a model id from `models audio list`, in quotes"))),
63
+ speech_model: v.optional(v.string(plain("has to be a model id from `models list`, in quotes"))),
64
64
  search_catch_up: v.optional(flag),
65
65
  picture_search: v.optional(flag),
66
66
  ...AI_ENTRIES,
package/dist/cli/steps.js CHANGED
@@ -83,7 +83,7 @@ export const STEPS = defineSteps([
83
83
  },
84
84
  {
85
85
  id: "models.download",
86
- command: "models text download",
86
+ command: "models download",
87
87
  args: [{ name: "model", required: true }],
88
88
  why: "{model} is not downloaded, so nothing is searched by meaning",
89
89
  },
@@ -116,7 +116,7 @@ export const STEPS = defineSteps([
116
116
  },
117
117
  {
118
118
  id: "store.reindex",
119
- command: "store reindex",
119
+ command: "store rebuild",
120
120
  args: [],
121
121
  why: "the stems were built by another stemmer; this builds them again",
122
122
  },
@@ -134,7 +134,7 @@ export const STEPS = defineSteps([
134
134
  },
135
135
  {
136
136
  id: "conversations.build",
137
- command: "conversations build",
137
+ command: "conversations links rebuild",
138
138
  args: [{ name: "chat", required: false }],
139
139
  why: "conversations are missing or older than the messages; this builds them",
140
140
  },
@@ -175,7 +175,7 @@ export const STEPS = defineSteps([
175
175
  },
176
176
  {
177
177
  id: "doctor.report-create",
178
- command: "doctor report create",
178
+ command: "doctor reports create",
179
179
  args: [{ name: "run", required: false }],
180
180
  why: "writes the report to a file to read and send; it holds no message text",
181
181
  },
@@ -132,7 +132,7 @@ const changeCategory = async (app, env, setting, value) => {
132
132
  return {
133
133
  result: { store: storePath(env), file, scope: "shared", setting, value: set ? taskCategoryValue(set) : null },
134
134
  note: [
135
- `saved in ${file} — rules, tasks add and tasks plan in tg, MAX and memo take categories from it; ` +
135
+ `saved in ${file} — rules, tasks add and tasks update in tg, MAX and memo take categories from it; ` +
136
136
  "tasks keep the category they have",
137
137
  ...unlisted,
138
138
  ].join("\n"),
@@ -32,7 +32,6 @@ export const restartServers = (app, script, env, spawn) => {
32
32
  };
33
33
  export const upgradeCommand = (app, packageName, contextFor) => grantsAuthority(annotate(new Command("upgrade"), {
34
34
  effects: LOCAL_WRITE,
35
- mcp: false,
36
35
  offline: false,
37
36
  examples: [`${app.command} upgrade --check`, `${app.command} upgrade`],
38
37
  }), { unless: "check" })