@wirecat/cli-core 0.405.0 → 0.407.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 (127) hide show
  1. package/CHANGELOG.md +36 -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 +8 -0
  5. package/dist/cli/answer-entity.js +31 -0
  6. package/dist/cli/command-contract.js +5 -6
  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 +6 -2
  13. package/dist/cli/index.js +6 -2
  14. package/dist/cli/json-fields.js +2 -2
  15. package/dist/cli/meeting-batch.d.ts +1 -1
  16. package/dist/cli/meeting-batch.js +20 -2
  17. package/dist/cli/meeting-project-candidates-command.js +0 -1
  18. package/dist/cli/meeting-results.d.ts +3 -0
  19. package/dist/cli/meeting-results.js +31 -0
  20. package/dist/cli/meeting-series-inventory-command.js +0 -1
  21. package/dist/cli/messenger/admin-commands.js +14 -14
  22. package/dist/cli/messenger/archive-commands.js +2 -2
  23. package/dist/cli/messenger/attachments-command.js +7 -8
  24. package/dist/cli/messenger/backfill-command.js +1 -1
  25. package/dist/cli/messenger/chat-answers.js +3 -3
  26. package/dist/cli/messenger/chat-lists.js +3 -3
  27. package/dist/cli/messenger/chats-command.js +8 -8
  28. package/dist/cli/messenger/chats-members-command.js +1 -1
  29. package/dist/cli/messenger/contacts-command.js +10 -7
  30. package/dist/cli/messenger/context.d.ts +11 -4
  31. package/dist/cli/messenger/context.js +6 -0
  32. package/dist/cli/messenger/conversation-examples.js +7 -6
  33. package/dist/cli/messenger/conversations-command.d.ts +0 -4
  34. package/dist/cli/messenger/conversations-command.js +130 -135
  35. package/dist/cli/messenger/counters-command.js +4 -2
  36. package/dist/cli/messenger/doctor-command.js +8 -4
  37. package/dist/cli/messenger/gaps-command.js +2 -2
  38. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  39. package/dist/cli/messenger/hearing-command.js +1 -1
  40. package/dist/cli/messenger/images-command.js +1 -1
  41. package/dist/cli/messenger/inbox.js +1 -3
  42. package/dist/cli/messenger/lacks.d.ts +1 -0
  43. package/dist/cli/messenger/lacks.js +1 -1
  44. package/dist/cli/messenger/mcp-command.js +4 -4
  45. package/dist/cli/messenger/messages-comments-command.js +10 -5
  46. package/dist/cli/messenger/metadata-command.js +5 -5
  47. package/dist/cli/messenger/models-command.js +101 -90
  48. package/dist/cli/messenger/private-people-command.js +5 -6
  49. package/dist/cli/messenger/replies-edit-command.js +0 -1
  50. package/dist/cli/messenger/review.js +1 -3
  51. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  52. package/dist/cli/messenger/stats-command.js +1 -1
  53. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  54. package/dist/cli/messenger/store-maintenance-command.js +4 -4
  55. package/dist/cli/messenger/stored.js +5 -0
  56. package/dist/cli/messenger/tags-command.js +4 -10
  57. package/dist/cli/messenger/tasks-command.js +3 -3
  58. package/dist/cli/messenger/transcribe-command.js +1 -1
  59. package/dist/cli/moved.d.ts +2 -0
  60. package/dist/cli/moved.js +39 -1
  61. package/dist/cli/permission-keys.js +2 -0
  62. package/dist/cli/result-fields.js +19 -1
  63. package/dist/cli/settings.js +1 -1
  64. package/dist/cli/steps.js +4 -4
  65. package/dist/cli/store-settings.js +1 -1
  66. package/dist/cli/upgrade-command.js +0 -1
  67. package/dist/domain/models.d.ts +3 -3
  68. package/dist/domain/task-categories.js +1 -1
  69. package/dist/embeddings/remote.js +1 -1
  70. package/dist/entities/contact.d.ts +6 -0
  71. package/dist/entities/contact.js +3 -0
  72. package/dist/entities/index.d.ts +139 -0
  73. package/dist/entities/index.js +4 -1
  74. package/dist/entities/member.d.ts +2 -2
  75. package/dist/entities/member.js +2 -2
  76. package/dist/entities/store-records.d.ts +11 -0
  77. package/dist/entities/store-records.js +1 -0
  78. package/dist/mcp/agent.d.ts +1 -0
  79. package/dist/mcp/agent.js +1 -1
  80. package/dist/mcp/tools/attachments.js +1 -1
  81. package/dist/meetings/cli/index.js +10 -3
  82. package/dist/meetings/index.d.ts +1 -0
  83. package/dist/meetings/index.js +1 -0
  84. package/dist/meetings/result-items.d.ts +127 -0
  85. package/dist/meetings/result-items.js +125 -0
  86. package/dist/ports/app.d.ts +1 -1
  87. package/dist/ports/messenger.d.ts +6 -1
  88. package/dist/search/task-similarity.js +1 -1
  89. package/dist/sends/guard.js +2 -2
  90. package/dist/sends/permission-aliases.d.ts +5 -0
  91. package/dist/sends/permission-aliases.js +49 -0
  92. package/dist/sends/permissions.js +33 -25
  93. package/dist/services/admin.js +7 -7
  94. package/dist/services/chats.js +1 -1
  95. package/dist/services/conversations.d.ts +1 -1
  96. package/dist/services/conversations.js +1 -1
  97. package/dist/services/counters.js +1 -1
  98. package/dist/services/embeddings-maintenance.js +2 -2
  99. package/dist/services/embeddings-search.js +1 -1
  100. package/dist/services/embeddings.d.ts +4 -4
  101. package/dist/services/messages-search.d.ts +1 -1
  102. package/dist/services/messages-search.js +2 -2
  103. package/dist/services/metadata.js +2 -2
  104. package/dist/services/notes-search.js +3 -3
  105. package/dist/services/search-catchup.js +1 -1
  106. package/dist/speech/install.d.ts +4 -2
  107. package/dist/speech/install.js +16 -4
  108. package/dist/speech/local.js +1 -1
  109. package/dist/speech/transcribe.js +1 -1
  110. package/dist/store/search-index.d.ts +1 -1
  111. package/dist/store/sqlite/attachment-texts.d.ts +1 -1
  112. package/dist/store/sqlite/attachment-texts.js +1 -1
  113. package/dist/store/sqlite/batches.js +1 -1
  114. package/dist/store/sqlite/involvements.d.ts +1 -1
  115. package/dist/store/sqlite/note-index.d.ts +2 -2
  116. package/dist/store/sqlite/note-index.js +2 -2
  117. package/dist/store/sqlite/search-index.d.ts +1 -1
  118. package/dist/store/sqlite/search-index.js +1 -1
  119. package/dist/store/sqlite/stems-fill.d.ts +3 -3
  120. package/dist/store/sqlite/stems-fill.js +3 -3
  121. package/dist/store/sqlite/stems.d.ts +1 -1
  122. package/dist/store/sqlite/task-pairs.d.ts +1 -1
  123. package/dist/store/sqlite/task-pairs.js +1 -1
  124. package/package.json +1 -1
  125. package/parity.json +130 -139
  126. package/skills/follow-ups/SKILL.md +5 -5
  127. package/skills/link-conversations/SKILL.md +8 -8
@@ -21,7 +21,7 @@ const count = (value) => {
21
21
  };
22
22
  export const gapsCommand = (messenger) => {
23
23
  const group = new Command("gaps").description("inspect recorded interior coverage gaps and explicitly fetch them");
24
- annotate(group.command("plan"), { output: { entity: "store_gap_plan" }, effects: LOCAL_READ })
24
+ annotate(group.command("show"), { output: { entity: "store_gap_plan" }, effects: LOCAL_READ })
25
25
  .description("local coverage plan; missing message ids alone do not imply missing history")
26
26
  .argument("<chat>", messenger.chatArgument)
27
27
  .action(async function (chat) {
@@ -71,7 +71,7 @@ export const gapsCommand = (messenger) => {
71
71
  if (prepare) {
72
72
  validateCatchUpBounds(preparation);
73
73
  refuseLocalWrite(context, messenger.app.command, "conversations.links");
74
- refuseLocalWrite(context, messenger.app.command, "conversations.embed");
74
+ refuseLocalWrite(context, messenger.app.command, "conversations.embeddings");
75
75
  }
76
76
  if (args.background) {
77
77
  const planned = await context.withServices((services) => services.gaps.plan(chat));
@@ -2,7 +2,7 @@ import type { Message } from "../../domain/models.js";
2
2
  import { type Hearing } from "../../speech/hearing.js";
3
3
  import type { Messenger, MessengerContext, ReadConnection } from "./context.js";
4
4
  export declare const TRANSCRIBE_OPTION: readonly ["--transcribe", "turn voice messages not heard yet into text — by the messenger where it can, else by a model on this machine; can take minutes"];
5
- export declare const MODEL_OPTION: readonly ["--model <model>", "which downloaded speech model hears them, with --transcribe; `models audio list` shows them"];
5
+ export declare const MODEL_OPTION: readonly ["--model <model>", "which downloaded speech model hears them, with --transcribe; `models list` shows them"];
6
6
  /**
7
7
  * Kept transcripts on every read; new ones with `--transcribe`, using the read's connection when
8
8
  * supplied. It is released before local recognition. `undefined` when there is no voice message, and then no file is opened.
@@ -6,7 +6,7 @@ export const TRANSCRIBE_OPTION = [
6
6
  ];
7
7
  export const MODEL_OPTION = [
8
8
  "--model <model>",
9
- "which downloaded speech model hears them, with --transcribe; `models audio list` shows them",
9
+ "which downloaded speech model hears them, with --transcribe; `models list` shows them",
10
10
  ];
11
11
  /**
12
12
  * Kept transcripts on every read; new ones with `--transcribe`, using the read's connection when
@@ -27,7 +27,7 @@ export const imagesCommand = (messenger) => {
27
27
  .action(async function () {
28
28
  const { chat, limit = DEFAULT_LIMIT } = this.opts();
29
29
  const context = messengerContext(this, messenger);
30
- refuseLocalWrite(context, messenger.app.command, "conversations.embed");
30
+ refuseLocalWrite(context, messenger.app.command, "conversations.embeddings");
31
31
  const controller = new AbortController();
32
32
  context.track({ close: async () => controller.abort() });
33
33
  const done = await context.withServices((services) => services.pictures.embed({ ...(chat === undefined ? {} : { chat }), limit, signal: controller.signal }));
@@ -1,8 +1,7 @@
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 { renderMessages } from "../../render/messages.js";
5
- import { CHAT_KINDS } from "../../services/chats.js";
6
5
  import { kindsOf } from "../../services/inbox.js";
7
6
  import { modelWith } from "../../speech/hearing.js";
8
7
  import { REMOTE_READ } from "../effects.js";
@@ -41,7 +40,6 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
41
40
  .option("--new", "what arrived since the last check, each message once — for scheduled runs")
42
41
  .option("--limit <n>", "at most this many per chat, the newest", positiveCount("--limit"))
43
42
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
44
- .addOption(new Option("--kind <kinds>", `compatibility spelling for kind:; ${CHAT_KINDS.join(", ")}`).hideHelp())
45
43
  .option(...TRANSCRIBE_OPTION)
46
44
  .option(...MODEL_OPTION)
47
45
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
@@ -9,6 +9,7 @@ type Need = readonly [method: keyof MessengerAdapter, what: string];
9
9
  export declare const COMMAND_NEEDS: Readonly<Record<string, Need>>;
10
10
  interface Lacking {
11
11
  channelComments?: boolean;
12
+ readsComments?: boolean;
12
13
  lacks?: readonly (keyof MessengerAdapter)[];
13
14
  never?: readonly (keyof MessengerAdapter)[];
14
15
  history?: "server" | "store";
@@ -30,7 +30,7 @@ export const lackedNeed = (messenger, path) => {
30
30
  const need = COMMAND_NEEDS[path];
31
31
  if (!need)
32
32
  return undefined;
33
- if (path === "messages comments" && messenger?.channelComments === false)
33
+ if (path === "messages comments" && (messenger?.readsComments ?? messenger?.channelComments) === false)
34
34
  return need;
35
35
  if (!messenger?.lacks?.includes(need[0]) && !messenger?.never?.includes(need[0]))
36
36
  return undefined;
@@ -74,9 +74,9 @@ export const mcpCommand = (messenger) => {
74
74
  // Pasted into a file, so a person gets the client's own JSON, indented; a script finds it under `config`.
75
75
  renderAnswer(this, context, itemOf({ config: entry.config }, { next: entry.nodeBound ? [nextStep("mcp.config", { args: {} })] : [] }), () => JSON.stringify(entry.config, null, 2));
76
76
  }));
77
- command.addCommand(withFlags(grantsAuthority(annotate(new Command("setup"), {
77
+ command.addCommand(withFlags(grantsAuthority(annotate(new Command("add"), {
78
78
  effects: LOCAL_WRITE,
79
- examples: [`${app.command} personal mcp setup codex --allow-writes`],
79
+ examples: [`${app.command} personal mcp add codex --allow-writes`],
80
80
  }))
81
81
  .description("add this profile's local MCP server to Codex or Claude Code")
82
82
  .argument("<client>", "codex or claude-code")
@@ -108,9 +108,9 @@ export const mcpCommand = (messenger) => {
108
108
  throw new CliError("validation_error", error instanceof Error ? error.message : "MCP setup failed");
109
109
  }
110
110
  }));
111
- command.addCommand(withFlags(annotate(new Command("doctor"), {
111
+ command.addCommand(withFlags(annotate(new Command("check"), {
112
112
  effects: LOCAL_READ,
113
- examples: [`${app.command} personal mcp doctor`],
113
+ examples: [`${app.command} personal mcp check`],
114
114
  }).description("check this profile's local MCP handshake and tool list")).action(async function () {
115
115
  const context = messengerContext(this, messenger);
116
116
  const { settings, env } = context;
@@ -10,15 +10,20 @@ import { messengerContext } from "./context.js";
10
10
  import { COMMENTS_QUERY, cursorOf, historyWindow } from "./history-query.js";
11
11
  import { messageItems, viewOf } from "./message-items.js";
12
12
  export const commentsCommand = (messenger) => {
13
+ const otherPosts = messenger.readsComments === true && messenger.channelComments === false;
13
14
  const command = annotate(new Command("comments"), {
14
- examples: [`${messenger.app.command} personal messages comments "News channel" 101 --limit 20`],
15
+ examples: [
16
+ `${messenger.app.command} personal messages comments "${otherPosts ? "Announcements" : "News channel"}" 101 --limit 20`,
17
+ ],
15
18
  effects: REMOTE_READ,
16
19
  })
17
- .summary("comments on a channel post")
18
- .description("the comments under a channel post, oldest to newest; they live in its discussion group")
20
+ .summary(otherPosts ? "comments on a post" : "comments on a channel post")
21
+ .description(otherPosts
22
+ ? "the comments received under a post, oldest to newest"
23
+ : "the comments under a channel post, oldest to newest; they live in its discussion group")
19
24
  .addHelpText("after", queryHelp(COMMENTS_QUERY))
20
- .argument("<chat>", `the channel: ${messenger.chatArgument}`)
21
- .argument("<post>", "the post's message id in the channel")
25
+ .argument("<chat>", `the ${otherPosts ? "chat" : "channel"}: ${messenger.chatArgument}`)
26
+ .argument("<post>", otherPosts ? "the post's message id in the chat" : "the post's message id in the channel")
22
27
  .argument("[query...]", "where to start: id<42")
23
28
  .option("--limit <n>", "how many", positiveCount("--limit"))
24
29
  .option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
@@ -8,10 +8,10 @@ import { messengerContext, refuseLocalWrite } from "./context.js";
8
8
  export const metadataCommand = (messenger) => {
9
9
  const tool = messenger.app.command;
10
10
  const command = new Command("metadata").description("cached group/channel descriptions for local automatic tags");
11
- annotate(command.command("get"), {
11
+ annotate(command.command("show"), {
12
12
  output: { entity: "chat_metadata" },
13
13
  effects: LOCAL_READ,
14
- examples: [`${tool} metadata get --chat 7`],
14
+ examples: [`${tool} metadata show --chat 7`],
15
15
  })
16
16
  .description("a stored chat's cached description, as automatic tags read it")
17
17
  .requiredOption("--chat <chat>", "a stored chat")
@@ -20,10 +20,10 @@ export const metadataCommand = (messenger) => {
20
20
  const { chat } = this.opts();
21
21
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.metadata.get(chat)))));
22
22
  });
23
- annotate(command.command("refresh"), {
23
+ annotate(command.command("sync"), {
24
24
  output: { entity: "chat_metadata_refresh" },
25
25
  effects: FETCH,
26
- examples: [`${tool} metadata refresh --chat 7 --chat 8`, `${tool} metadata refresh --only-missing --limit 20`],
26
+ examples: [`${tool} metadata sync --chat 7 --chat 8`, `${tool} metadata sync --only-missing --limit 20`],
27
27
  })
28
28
  .description("fetch group and channel descriptions again and cache them")
29
29
  .option("--chat <chat>", "stored group/channel; repeat for several", (value, previous) => [...previous, value], [])
@@ -31,7 +31,7 @@ export const metadataCommand = (messenger) => {
31
31
  .option("--limit <n>", "process at most 1–500 chats", positiveCount("--limit"), 50)
32
32
  .action(async function () {
33
33
  const context = messengerContext(this, messenger);
34
- refuseLocalWrite(context, messenger.app.command, "metadata.refresh");
34
+ refuseLocalWrite(context, messenger.app.command, "metadata.sync");
35
35
  const options = this.opts();
36
36
  const limit = Number(options.limit);
37
37
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
@@ -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',
@@ -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");