@wirecat/cli-core 0.404.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 (124) hide show
  1. package/CHANGELOG.md +45 -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/first-run.js +3 -3
  12. package/dist/cli/help/command-help.js +2 -2
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-fields.js +2 -2
  16. package/dist/cli/meeting-project-candidates-command.js +0 -1
  17. package/dist/cli/meeting-series-inventory-command.js +0 -1
  18. package/dist/cli/messenger/admin-commands.js +14 -14
  19. package/dist/cli/messenger/archive-commands.js +2 -2
  20. package/dist/cli/messenger/attachments-command.js +7 -8
  21. package/dist/cli/messenger/backfill-command.js +1 -1
  22. package/dist/cli/messenger/chat-answers.js +3 -3
  23. package/dist/cli/messenger/chat-lists.js +3 -3
  24. package/dist/cli/messenger/chats-command.js +8 -8
  25. package/dist/cli/messenger/chats-members-command.js +1 -1
  26. package/dist/cli/messenger/contacts-command.js +10 -7
  27. package/dist/cli/messenger/context.d.ts +11 -4
  28. package/dist/cli/messenger/context.js +6 -0
  29. package/dist/cli/messenger/conversation-examples.js +7 -6
  30. package/dist/cli/messenger/conversations-command.d.ts +0 -4
  31. package/dist/cli/messenger/conversations-command.js +130 -135
  32. package/dist/cli/messenger/counters-command.js +4 -2
  33. package/dist/cli/messenger/doctor-command.js +8 -4
  34. package/dist/cli/messenger/gaps-command.js +2 -2
  35. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  36. package/dist/cli/messenger/hearing-command.js +1 -1
  37. package/dist/cli/messenger/images-command.js +1 -1
  38. package/dist/cli/messenger/inbox.js +1 -3
  39. package/dist/cli/messenger/lacks.d.ts +1 -0
  40. package/dist/cli/messenger/lacks.js +1 -1
  41. package/dist/cli/messenger/mcp-command.js +4 -4
  42. package/dist/cli/messenger/messages-comments-command.js +10 -5
  43. package/dist/cli/messenger/messages-send-command.js +44 -3
  44. package/dist/cli/messenger/metadata-command.js +5 -5
  45. package/dist/cli/messenger/models-command.js +101 -90
  46. package/dist/cli/messenger/private-people-command.js +5 -6
  47. package/dist/cli/messenger/replies-edit-command.js +0 -1
  48. package/dist/cli/messenger/review.js +1 -3
  49. package/dist/cli/messenger/serve-relay.d.ts +2 -0
  50. package/dist/cli/messenger/stats-command.js +1 -1
  51. package/dist/cli/messenger/store-maintenance-command.d.ts +2 -2
  52. package/dist/cli/messenger/store-maintenance-command.js +4 -4
  53. package/dist/cli/messenger/stored.js +5 -0
  54. package/dist/cli/messenger/tags-command.js +4 -10
  55. package/dist/cli/messenger/tasks-command.js +3 -3
  56. package/dist/cli/messenger/transcribe-command.js +1 -1
  57. package/dist/cli/moved.d.ts +2 -0
  58. package/dist/cli/moved.js +39 -1
  59. package/dist/cli/permission-keys.js +2 -0
  60. package/dist/cli/settings.js +1 -1
  61. package/dist/cli/setup-answers.d.ts +2 -2
  62. package/dist/cli/setup-answers.js +2 -2
  63. package/dist/cli/steps.js +4 -4
  64. package/dist/cli/store-settings.js +1 -1
  65. package/dist/cli/upgrade-command.js +0 -1
  66. package/dist/domain/models.d.ts +3 -3
  67. package/dist/domain/task-categories.js +1 -1
  68. package/dist/embeddings/remote.js +1 -1
  69. package/dist/entities/index.d.ts +380 -0
  70. package/dist/entities/index.js +2 -1
  71. package/dist/entities/member.d.ts +2 -2
  72. package/dist/entities/member.js +2 -2
  73. package/dist/entities/message-write.d.ts +386 -0
  74. package/dist/entities/message-write.js +6 -0
  75. package/dist/mcp/agent.d.ts +1 -0
  76. package/dist/mcp/agent.js +1 -1
  77. package/dist/mcp/tools/attachments.js +1 -1
  78. package/dist/meetings/mcp/index.d.ts +15 -15
  79. package/dist/ports/app.d.ts +1 -1
  80. package/dist/ports/messenger.d.ts +8 -1
  81. package/dist/ports/traits.d.ts +2 -0
  82. package/dist/search/task-similarity.js +1 -1
  83. package/dist/sends/guard.js +2 -2
  84. package/dist/sends/permission-aliases.d.ts +5 -0
  85. package/dist/sends/permission-aliases.js +49 -0
  86. package/dist/sends/permissions.d.ts +1 -1
  87. package/dist/sends/permissions.js +35 -25
  88. package/dist/services/admin.js +7 -7
  89. package/dist/services/chats.js +1 -1
  90. package/dist/services/conversations.d.ts +1 -1
  91. package/dist/services/conversations.js +1 -1
  92. package/dist/services/counters.js +1 -1
  93. package/dist/services/embeddings-maintenance.js +2 -2
  94. package/dist/services/embeddings-search.js +1 -1
  95. package/dist/services/embeddings.d.ts +4 -4
  96. package/dist/services/messages-search.d.ts +1 -1
  97. package/dist/services/messages-search.js +2 -2
  98. package/dist/services/messages.d.ts +5 -0
  99. package/dist/services/messages.js +23 -0
  100. package/dist/services/metadata.js +2 -2
  101. package/dist/services/notes-search.js +3 -3
  102. package/dist/services/search-catchup.js +1 -1
  103. package/dist/speech/install.d.ts +4 -2
  104. package/dist/speech/install.js +16 -4
  105. package/dist/speech/local.js +1 -1
  106. package/dist/speech/transcribe.js +1 -1
  107. package/dist/store/search-index.d.ts +1 -1
  108. package/dist/store/sqlite/attachment-texts.d.ts +1 -1
  109. package/dist/store/sqlite/attachment-texts.js +1 -1
  110. package/dist/store/sqlite/batches.js +1 -1
  111. package/dist/store/sqlite/involvements.d.ts +1 -1
  112. package/dist/store/sqlite/note-index.d.ts +2 -2
  113. package/dist/store/sqlite/note-index.js +2 -2
  114. package/dist/store/sqlite/search-index.d.ts +1 -1
  115. package/dist/store/sqlite/search-index.js +1 -1
  116. package/dist/store/sqlite/stems-fill.d.ts +3 -3
  117. package/dist/store/sqlite/stems-fill.js +3 -3
  118. package/dist/store/sqlite/stems.d.ts +1 -1
  119. package/dist/store/sqlite/task-pairs.d.ts +1 -1
  120. package/dist/store/sqlite/task-pairs.js +1 -1
  121. package/package.json +1 -1
  122. package/parity.json +186 -179
  123. package/skills/follow-ups/SKILL.md +5 -5
  124. package/skills/link-conversations/SKILL.md +8 -8
@@ -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;
@@ -30,94 +30,121 @@ import { readAll } from "./stdin.js";
30
30
  * `conversations`: the threads inside a group chat, found by rules over the stored messages — replies,
31
31
  * mentions, one sender's messages in a row. From the store alone; nothing is built until asked (phase 3).
32
32
  */
33
+ const conversationBuildCommand = (messenger) => boundOptions(reachesOtherService(annotate(new Command("rebuild"), { output: { entity: "conversation_build" }, effects: REMOTE_WRITE }))
34
+ .summary("build conversation links from stored messages")
35
+ .description("find a chat's conversations in what the store holds, replacing the last build; without --chat, every " +
36
+ "chat that changed since its build and every group never built; never asks the messenger")
37
+ .option("--chat <chat>", messenger.chatArgument)
38
+ .option("--analyze", "link batches using the configured analysis provider; requires --chat and remembers consent for this chat/provider")
39
+ .addOption(new Option("--analysis-provider <provider>", "analysis: agent, openai or anthropic"))
40
+ .option("--model <model>", "analysis model; overrides analysis_model")
41
+ .option("--base-url <url>", "analysis API endpoint; overrides analysis_base_url")
42
+ .option("--size <n>", "analysis answer messages per batch, 10–200; default 50", positiveCount("--size"))
43
+ .option("--max-tokens <n>", "analysis input/output reservation cap per run; default 100000", positiveCount("--max-tokens")), { chunks: false }).action(async function () {
44
+ const options = this.optsWithGlobals();
45
+ const { chat, maxChats } = options;
46
+ const context = messengerContext(this, messenger);
47
+ if (options.analyze) {
48
+ if (!chat)
49
+ throw new CliError("validation_error", "--analyze requires --chat");
50
+ refuseLocalWrite(context, messenger.app.command, LINKS_KEY);
51
+ refuseLocalWrite(context, messenger.app.command, "conversations.links.rebuild");
52
+ const target = analysisChoice(options, messenger.app, context.settings, context.env, context.keyring);
53
+ if (!isLocal(target.baseUrl))
54
+ refuseUnderReadOnly(this, `send chat text to ${new URL(target.baseUrl).host}`);
55
+ const controller = new AbortController();
56
+ context.track({ close: async () => controller.abort() });
57
+ const maxTokens = options.maxTokens ?? 100_000;
58
+ const size = options.size ?? BATCH_SIZE.default;
59
+ const result = await context.withStore(async (store, account) => {
60
+ const services = servicesFor({
61
+ ...storedDeps(messenger, store, account, context.guard),
62
+ env: context.env,
63
+ profile: context.profile,
64
+ });
65
+ const status = await services.conversations.batchStatus(chat, size);
66
+ const consents = analysisConsents(store, account);
67
+ const identity = providerIdentity(target);
68
+ if (status.messages && !(await consents.has(status.chat, identity))) {
69
+ const what = `${status.messages} messages, ${status.characters} characters (about ${status.tokensEstimate} message tokens, plus prompt and batch context), go to ${target.model} at ${target.baseUrl}; this run reserves at most ${maxTokens} input/output tokens. Consent is remembered for this chat and provider until revoked`;
70
+ if (!options.yes) {
71
+ const answer = await askOwner(this, `${what}. Allow analysis? [y/N] `);
72
+ if (answer === null)
73
+ throw new CliError("confirmation_required", `${what} — add --yes`);
74
+ if (!/^y(es)?$/i.test(answer.trim()))
75
+ throw new CliError("cancelled", "cancelled — nothing was sent");
76
+ }
77
+ await consents.remember(status.chat, identity);
78
+ }
79
+ await services.conversations.build(chat);
80
+ const request = openAnalysis(target, controller.signal);
81
+ return runAnalysis(services.conversations, chat, async (...args) => {
82
+ if (!(await consents.has(status.chat, identity)))
83
+ throw new CliError("permission_error", "analysis consent was revoked; no further batch was sent");
84
+ return request(...args);
85
+ }, {
86
+ model: target.model,
87
+ command: messenger.app.command,
88
+ size,
89
+ maxTokens,
90
+ });
91
+ });
92
+ renderAnswer(this, context, itemOf(snakeKeys(result)));
93
+ return;
94
+ }
95
+ if (options.analysisProvider !== undefined ||
96
+ options.provider !== undefined ||
97
+ options.model !== undefined ||
98
+ options.baseUrl !== undefined ||
99
+ options.size !== undefined ||
100
+ options.maxTokens !== undefined)
101
+ throw new CliError("validation_error", "analysis options require --analyze");
102
+ if (chat === undefined) {
103
+ const refreshed = await context.withServices((services) => services.embeddings.refresh({
104
+ embed: false,
105
+ ...(maxChats ? { maxChats } : {}),
106
+ progress: (note) => context.renderer.note(note),
107
+ }));
108
+ renderRefreshed(this, context, refreshed);
109
+ return;
110
+ }
111
+ const built = await context.withServices((services) => services.conversations.build(chat));
112
+ renderAnswer(this, context, itemOf(snakeKeys(built)), () => `${built.messages} messages → ${built.conversations} conversations, ${built.links} links (rules v${built.rulesVersion})\n`);
113
+ });
114
+ const embeddingStatusCommand = (messenger) => withModelOptions(annotate(new Command("status"), { output: { entity: "conversation_embedding_status" }, effects: LOCAL_READ })
115
+ .summary("which conversation vectors are ready or stale")
116
+ .description("how many chunks of a chat have a vector of the model, how many are left, and what is left costs")
117
+ .option("--chat <chat>", messenger.chatArgument)).action(async function () {
118
+ const options = this.optsWithGlobals();
119
+ const chat = chatOf(options);
120
+ const context = messengerContext(this, messenger);
121
+ const model = choiceOf(options, messenger, context, { needKey: false });
122
+ const status = await context.withServices((services) => services.embeddings.status(chat, model));
123
+ renderAnswer(this, context, itemOf(snakeKeys(status)), () => `${status.embedded} of ${status.chunks} chunks embedded with ${status.model}; ${status.left} left` +
124
+ (status.estimateSeconds === null ? "" : `, ${minutes(status.estimateSeconds)}`) +
125
+ (status.priceAtMost ? `, at most ${dollars(status.priceAtMost.usd)}` : "") +
126
+ "\n");
127
+ });
128
+ const embeddingClearCommand = (messenger) => withModelOptions(destroysNothing(annotate(new Command("clear"), { output: { entity: "conversation_change" }, effects: LOCAL_WRITE }))
129
+ .summary("remove stored conversation vectors")
130
+ .description("drop a chat's vectors, or only one model's; messages and conversations are never touched")
131
+ .option("--chat <chat>", messenger.chatArgument)).action(async function () {
132
+ const options = this.optsWithGlobals();
133
+ const chat = chatOf(options);
134
+ const context = messengerContext(this, messenger);
135
+ refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
136
+ const given = options.model !== undefined ||
137
+ options.embeddingProvider !== undefined ||
138
+ options.provider !== undefined ||
139
+ options.baseUrl !== undefined;
140
+ const model = given ? choiceOf(options, messenger, context, { needKey: false }) : undefined;
141
+ const cleared = await context.withServices((services) => services.embeddings.clear(chat, model));
142
+ renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} vectors dropped\n`);
143
+ });
33
144
  export const conversationsCommand = (messenger) => {
34
145
  const conversations = new Command("conversations")
35
146
  .summary("group stored messages into conversations")
36
147
  .description("the conversations inside a chat, found in the stored messages by replies, mentions and who wrote next");
37
- boundOptions(reachesOtherService(annotate(conversations.command("build"), { output: { entity: "conversation_build" }, effects: REMOTE_WRITE }))
38
- .summary("build conversation links from stored messages")
39
- .description("find a chat's conversations in what the store holds, replacing the last build; without --chat, every " +
40
- "chat that changed since its build and every group never built; never asks the messenger")
41
- .option("--chat <chat>", messenger.chatArgument)
42
- .option("--analyze", "link batches using the configured analysis provider; requires --chat and remembers consent for this chat/provider")
43
- .addOption(new Option("--analysis-provider <provider>", "analysis: agent, openai or anthropic").conflicts("provider"))
44
- .addOption(new Option("--provider <provider>", "compatibility alias for --analysis-provider")
45
- .hideHelp()
46
- .conflicts("analysisProvider"))
47
- .option("--model <model>", "analysis model; overrides analysis_model")
48
- .option("--base-url <url>", "analysis API endpoint; overrides analysis_base_url")
49
- .option("--size <n>", "analysis answer messages per batch, 10–200; default 50", positiveCount("--size"))
50
- .option("--max-tokens <n>", "analysis input/output reservation cap per run; default 100000", positiveCount("--max-tokens")), { chunks: false }).action(async function () {
51
- const options = this.optsWithGlobals();
52
- const { chat, maxChats } = options;
53
- const context = messengerContext(this, messenger);
54
- if (options.analyze) {
55
- if (!chat)
56
- throw new CliError("validation_error", "--analyze requires --chat");
57
- refuseLocalWrite(context, messenger.app.command, LINKS_KEY);
58
- refuseLocalWrite(context, messenger.app.command, "conversations.build");
59
- const target = analysisChoice(options, messenger.app, context.settings, context.env, context.keyring);
60
- if (!isLocal(target.baseUrl))
61
- refuseUnderReadOnly(this, `send chat text to ${new URL(target.baseUrl).host}`);
62
- const controller = new AbortController();
63
- context.track({ close: async () => controller.abort() });
64
- const maxTokens = options.maxTokens ?? 100_000;
65
- const size = options.size ?? BATCH_SIZE.default;
66
- const result = await context.withStore(async (store, account) => {
67
- const services = servicesFor({
68
- ...storedDeps(messenger, store, account, context.guard),
69
- env: context.env,
70
- profile: context.profile,
71
- });
72
- const status = await services.conversations.batchStatus(chat, size);
73
- const consents = analysisConsents(store, account);
74
- const identity = providerIdentity(target);
75
- if (status.messages && !(await consents.has(status.chat, identity))) {
76
- const what = `${status.messages} messages, ${status.characters} characters (about ${status.tokensEstimate} message tokens, plus prompt and batch context), go to ${target.model} at ${target.baseUrl}; this run reserves at most ${maxTokens} input/output tokens. Consent is remembered for this chat and provider until revoked`;
77
- if (!options.yes) {
78
- const answer = await askOwner(this, `${what}. Allow analysis? [y/N] `);
79
- if (answer === null)
80
- throw new CliError("confirmation_required", `${what} — add --yes`);
81
- if (!/^y(es)?$/i.test(answer.trim()))
82
- throw new CliError("cancelled", "cancelled — nothing was sent");
83
- }
84
- await consents.remember(status.chat, identity);
85
- }
86
- await services.conversations.build(chat);
87
- const request = openAnalysis(target, controller.signal);
88
- return runAnalysis(services.conversations, chat, async (...args) => {
89
- if (!(await consents.has(status.chat, identity)))
90
- throw new CliError("permission_error", "analysis consent was revoked; no further batch was sent");
91
- return request(...args);
92
- }, {
93
- model: target.model,
94
- command: messenger.app.command,
95
- size,
96
- maxTokens,
97
- });
98
- });
99
- renderAnswer(this, context, itemOf(snakeKeys(result)));
100
- return;
101
- }
102
- if (options.analysisProvider !== undefined ||
103
- options.provider !== undefined ||
104
- options.model !== undefined ||
105
- options.baseUrl !== undefined ||
106
- options.size !== undefined ||
107
- options.maxTokens !== undefined)
108
- throw new CliError("validation_error", "analysis options require --analyze");
109
- if (chat === undefined) {
110
- const refreshed = await context.withServices((services) => services.embeddings.refresh({
111
- embed: false,
112
- ...(maxChats ? { maxChats } : {}),
113
- progress: (note) => context.renderer.note(note),
114
- }));
115
- renderRefreshed(this, context, refreshed);
116
- return;
117
- }
118
- const built = await context.withServices((services) => services.conversations.build(chat));
119
- renderAnswer(this, context, itemOf(snakeKeys(built)), () => `${built.messages} messages → ${built.conversations} conversations, ${built.links} links (rules v${built.rulesVersion})\n`);
120
- });
121
148
  withQuery(annotate(conversations.command("list"), { effects: LOCAL_READ })
122
149
  .summary("stored conversations in one chat")
123
150
  .description("a chat's conversations, the newest first: when, how many messages, how many people")
@@ -187,7 +214,7 @@ export const conversationsCommand = (messenger) => {
187
214
  const { model: id, chats, unbuiltGroups, } = await context.withServices((services) => services.embeddings.readiness({ ...(options.chat === undefined ? {} : { chat: options.chat }), model }));
188
215
  renderAnswer(this, context, readinessList({ model: id, chats, ...(unbuiltGroups === undefined ? {} : { unbuiltGroups }) }), () => chats.map((one) => `${readinessLine(one, id)}\n`).join(""));
189
216
  if (chats.length === 0 && !unbuiltGroups && context.format === "pretty")
190
- context.renderer.note("no chat has conversations yet — `conversations build --chat <chat>`");
217
+ context.renderer.note("no chat has conversations yet — `conversations links rebuild --chat <chat>`");
191
218
  });
192
219
  const batches = conversations
193
220
  .command("batches")
@@ -204,7 +231,7 @@ export const conversationsCommand = (messenger) => {
204
231
  renderAnswer(this, context, itemOf(snakeKeys(status)), () => `${status.messages} messages to answer, about ${status.batches} batches, ` +
205
232
  `${status.characters} characters (about ${status.tokensEstimate} tokens)\n`);
206
233
  });
207
- sizeOption(annotate(batches.command("next"), { output: { entity: "conversation_batch" }, effects: LOCAL_READ })
234
+ sizeOption(annotate(batches.command("show"), { output: { entity: "conversation_batch" }, effects: LOCAL_READ })
208
235
  .summary("get the next batch of messages to link")
209
236
  .description("the next window to answer, with the messages before it; message text goes to stdout only")
210
237
  .requiredOption("--chat <chat>", messenger.chatArgument)).action(async function () {
@@ -228,9 +255,8 @@ export const conversationsCommand = (messenger) => {
228
255
  .join("")}`);
229
256
  context.renderer.note(`${batch.remaining.messages} messages left after this batch`);
230
257
  });
231
- const links = conversations
232
- .command("links")
233
- .description("your agent's answers: which earlier message each message of a batch answers");
258
+ const links = conversations.command("links").description("message links and the conversations derived from them");
259
+ links.addCommand(conversationBuildCommand(messenger));
234
260
  readsStdin(annotate(links.command("add"), { output: { entity: "conversation_change" }, effects: LOCAL_WRITE }), {
235
261
  name: "answer",
236
262
  description: '{ "model", "skill"?, "answers": [{ "message", "parent", "confidence" }] }',
@@ -239,7 +265,7 @@ export const conversationsCommand = (messenger) => {
239
265
  .summary("save an agent's conversation-link answers")
240
266
  .description('store your agent\'s answer to a batch, read as JSON from stdin: { "model", "answers": [{ "message", ' +
241
267
  '"parent", "confidence" }] }; all or nothing')
242
- .requiredOption("--batch <id>", "the batch id `conversations batches next` printed")
268
+ .requiredOption("--batch <id>", "the batch id `conversations batches show` printed")
243
269
  .action(async function () {
244
270
  const { batch } = this.opts();
245
271
  const context = messengerContext(this, messenger);
@@ -252,8 +278,7 @@ export const conversationsCommand = (messenger) => {
252
278
  .summary("remove an agent's saved conversation-link answers")
253
279
  .description("drop your agent's answers for a chat, or only one model's; messages are never touched")
254
280
  .requiredOption("--chat <chat>", messenger.chatArgument)
255
- .addOption(new Option("--answer-model <model>", "only the stored answers this model gave").conflicts("model"))
256
- .addOption(new Option("--model <model>", "compatibility alias for --answer-model").hideHelp().conflicts("answerModel"))
281
+ .addOption(new Option("--answer-model <model>", "only the stored answers this model gave"))
257
282
  .action(async function () {
258
283
  const { chat, model, answerModel } = this.opts();
259
284
  const context = messengerContext(this, messenger);
@@ -308,36 +333,9 @@ export const conversationsCommand = (messenger) => {
308
333
  next: done.skipped ? [nextStep("conversations.build", { args: { chat } })] : [],
309
334
  }), () => `${done.embedded} chunks embedded with ${done.model}${done.skipped ? `, ${done.skipped} changed since the build` : ""}\n`);
310
335
  });
311
- withModelOptions(annotate(embed.command("status"), { output: { entity: "conversation_embedding_status" }, effects: LOCAL_READ })
312
- .summary("which conversation vectors are ready or stale")
313
- .description("how many chunks of a chat have a vector of the model, how many are left, and what is left costs")
314
- .option("--chat <chat>", messenger.chatArgument)).action(async function () {
315
- const options = this.optsWithGlobals();
316
- const chat = chatOf(options);
317
- const context = messengerContext(this, messenger);
318
- const model = choiceOf(options, messenger, context, { needKey: false });
319
- const status = await context.withServices((services) => services.embeddings.status(chat, model));
320
- renderAnswer(this, context, itemOf(snakeKeys(status)), () => `${status.embedded} of ${status.chunks} chunks embedded with ${status.model}; ${status.left} left` +
321
- (status.estimateSeconds === null ? "" : `, ${minutes(status.estimateSeconds)}`) +
322
- (status.priceAtMost ? `, at most ${dollars(status.priceAtMost.usd)}` : "") +
323
- "\n");
324
- });
325
- withModelOptions(destroysNothing(annotate(embed.command("clear"), { output: { entity: "conversation_change" }, effects: LOCAL_WRITE }))
326
- .summary("remove stored conversation vectors")
327
- .description("drop a chat's vectors, or only one model's; messages and conversations are never touched")
328
- .option("--chat <chat>", messenger.chatArgument)).action(async function () {
329
- const options = this.optsWithGlobals();
330
- const chat = chatOf(options);
331
- const context = messengerContext(this, messenger);
332
- refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
333
- const given = options.model !== undefined ||
334
- options.embeddingProvider !== undefined ||
335
- options.provider !== undefined ||
336
- options.baseUrl !== undefined;
337
- const model = given ? choiceOf(options, messenger, context, { needKey: false }) : undefined;
338
- const cleared = await context.withServices((services) => services.embeddings.clear(chat, model));
339
- renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} vectors dropped\n`);
340
- });
336
+ const embeddings = new Command("embeddings").description("stored conversation vectors");
337
+ embeddings.addCommand(embeddingStatusCommand(messenger)).addCommand(embeddingClearCommand(messenger));
338
+ conversations.addCommand(embeddings);
341
339
  boundOptions(destroysNothing(annotate(conversations.command("rebuild"), {
342
340
  output: { entity: "conversation_preparation" },
343
341
  effects: LOCAL_WRITE,
@@ -373,13 +371,13 @@ export const conversationsCommand = (messenger) => {
373
371
  has_more: false,
374
372
  }));
375
373
  });
376
- annotate(consents.command("revoke"), { effects: LOCAL_WRITE, output: { entity: "analysis_consent_revocation" } })
374
+ annotate(consents.command("remove"), {
375
+ effects: LOCAL_WRITE,
376
+ output: { entity: "analysis_consent_revocation" },
377
+ })
377
378
  .description("forget analysis permissions so the next analysis asks again")
378
379
  .option("--chat <chat>", "revoke only this chat's consents; defaults to every chat")
379
- .addOption(new Option("--consent-provider <identity>", "exact provider identity from consents list; defaults to every provider").conflicts("provider"))
380
- .addOption(new Option("--provider <identity>", "compatibility alias for --consent-provider")
381
- .hideHelp()
382
- .conflicts("consentProvider"))
380
+ .addOption(new Option("--consent-provider <identity>", "exact provider identity from consents list; defaults to every provider"))
383
381
  .action(async function () {
384
382
  const { chat, provider, consentProvider } = this.opts();
385
383
  const context = messengerContext(this, messenger);
@@ -406,11 +404,8 @@ const chatOf = ({ chat }) => {
406
404
  return chat;
407
405
  };
408
406
  const withModelOptions = (command) => command
409
- .option("--model <model>", "local: a model id from `models text list` (default: e5-small); remote: the provider's model")
410
- .addOption(new Option("--embedding-provider <provider>", "embedding provider: local or openai; flags override profile settings").conflicts("provider"))
411
- .addOption(new Option("--provider <provider>", "compatibility alias for --embedding-provider")
412
- .hideHelp()
413
- .conflicts("embeddingProvider"))
407
+ .option("--model <model>", "local: a model id from `models list` (default: e5-small); remote: the provider's model")
408
+ .addOption(new Option("--embedding-provider <provider>", "embedding provider: local or openai; flags override profile settings"))
414
409
  .option("--base-url <url>", "a server with OpenAI's /v1/embeddings: Gemini, Jina, or Ollama and LM Studio on this machine")
415
410
  .option("--dims <n>", "remote: the vector size — needed with --base-url; shortens an OpenAI model's", positiveCount("--dims"));
416
411
  /** How much one run over many chats may do: the owner's machine, not a background job, pays for it. */
@@ -488,7 +483,7 @@ const consent = async (command, status, baseUrl, { yes, maxTokens }) => {
488
483
  throw new CliError("cancelled", "cancelled — nothing was sent");
489
484
  };
490
485
  const LINKS_KEY = "conversations.links";
491
- const EMBED_KEY = "conversations.embed";
486
+ const EMBED_KEY = "conversations.embeddings";
492
487
  const dollars = (usd) => (usd < 0.0001 ? "under $0.0001" : `$${usd.toFixed(4)}`);
493
488
  const minutes = (seconds) => (seconds < 90 ? `about ${seconds} s` : `about ${Math.round(seconds / 60)} min`);
494
489
  /** The agent's JSON, shaped enough for the store to check the rest against the batch. */
@@ -524,7 +519,7 @@ export const linksCommand = (messenger) => annotate(new Command("links"), {
524
519
  return;
525
520
  }
526
521
  if (found.links.length === 0) {
527
- context.renderer.note("no links: it starts a conversation, or the chat is not built — `conversations build`");
522
+ context.renderer.note("no links: it starts a conversation, or the chat is not built — `conversations links rebuild`");
528
523
  }
529
524
  context.streams.data(found.links
530
525
  .map((link) => `${link.chosen ? "→" : " "} ${link.parentId ?? "(starts)"} ${link.source} ${link.kind} ` +
@@ -538,7 +533,7 @@ const itemLine = (one) => `${one.id} ${one.started_at.slice(0, 16).replace("T",
538
533
  const hitLine = ({ summary, chunk, score, stale, locator }) => `${score === null ? " — " : score.toFixed(3)} ${line(summary)} ` +
539
534
  `(messages ${chunk.firstMessageId}–${chunk.lastMessageId})${locator ? ` ${locator}` : ""}${stale ? " stale" : ""}`;
540
535
  const readinessLine = ({ chat, state, graph, pending, vectors }, model) => graph === null
541
- ? `${chat} ${state} — \`conversations build --chat ${chat}\``
536
+ ? `${chat} ${state} — \`conversations links rebuild --chat ${chat}\``
542
537
  : `${chat} ${state} built ${graph.builtAt.slice(0, 16).replace("T", " ")}, rules v${graph.rulesVersion}` +
543
538
  `${graph.outdatedRules ? " (outdated)" : ""} · not in the build: ${pending.new} new, ${pending.edited} edited, ` +
544
539
  `${pending.deleted} deleted · ${model}: ${vectors.current} current, ${vectors.stale} stale, ` +
@@ -9,7 +9,7 @@ import { countersAnswer, refreshAnswer, refuseJsonl } from "./stats-answers.js";
9
9
  export const countersCommand = (messenger) => {
10
10
  const parent = new Command("counters").description("per-counter observations and bounded remote refresh");
11
11
  for (const operation of ["show", "refresh"]) {
12
- const command = new Command(operation)
12
+ const command = new Command(operation === "refresh" ? "sync" : operation)
13
13
  .summary("refresh message counters from the messenger")
14
14
  .description(operation === "show"
15
15
  ? "show saved counter values and their observation freshness"
@@ -22,6 +22,8 @@ export const countersCommand = (messenger) => {
22
22
  .option("--selection <json>", "pinned counter-targets selection from counters show; conflicts with query and scope")
23
23
  .option("--counters <names>", "distinct views,reactions,comments fields; all three by default")
24
24
  .option("--limit <n>", "messages, 1–100; 20 by default", positiveCount("--limit"));
25
+ if (operation === "refresh")
26
+ command;
25
27
  if (operation === "show")
26
28
  command.option("--max-age <duration>", "maximum fresh observation age; 24h by default");
27
29
  annotate(command, operation === "refresh"
@@ -36,7 +38,7 @@ export const countersCommand = (messenger) => {
36
38
  const context = messengerContext(this, messenger), options = this.opts();
37
39
  refuseJsonl(context.format);
38
40
  if (operation === "refresh" && !options.dryRun)
39
- refuseLocalWrite(context, messenger.app.command, "stats.messages.counters.refresh");
41
+ refuseLocalWrite(context, messenger.app.command, "stats.messages.counters.sync");
40
42
  const query = {
41
43
  ...options,
42
44
  limit: options.limit ?? 20,
@@ -9,7 +9,7 @@ import { FloodMemory, floodPathFor } from "../../sends/flood.js";
9
9
  import { SendJournal, sendsPathFor } from "../../sends/journal.js";
10
10
  import { storePath } from "../../store/path.js";
11
11
  import { baseContext, environmentOf, outputFor } from "../context.js";
12
- import { destroysNothing, LOCAL_WRITE, REMOTE_READ } from "../effects.js";
12
+ import { destroysNothing, LOCAL_WRITE, NO_EFFECTS, REMOTE_READ } from "../effects.js";
13
13
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
14
14
  import { lineOf, nextStep } from "../steps.js";
15
15
  import { recalledAccount } from "./accounts.js";
@@ -57,11 +57,15 @@ const INCLUDES = [
57
57
  const EXCLUDES = ["message text", "chat titles", "names", "phone numbers", "the session", "the app credentials"];
58
58
  const reportCommand = (messenger) => {
59
59
  const { app } = messenger;
60
- const report = annotate(new Command("report"), { effects: REMOTE_READ })
60
+ const report = new Command("reports").description("problem reports");
61
+ annotate(report.command("show"), {
62
+ effects: NO_EFFECTS,
63
+ examples: [`${app.command} doctor reports show`],
64
+ })
61
65
  .description("what a problem report holds; writes nothing")
62
66
  .action(function () {
63
67
  const output = outputFor(this);
64
- const create = lineOf(this, "doctor report create");
68
+ const create = lineOf(this, "doctor reports create");
65
69
  renderAnswer(this, output, itemOf({ includes: INCLUDES, excludes: EXCLUDES, ...(app.issues ? { send_to: app.issues } : {}) }, { next: [nextStep("doctor.report-create", { args: {} })] }), () => [
66
70
  `A problem report is one file for the author of ${app.command}.`,
67
71
  "",
@@ -76,7 +80,7 @@ const reportCommand = (messenger) => {
76
80
  });
77
81
  destroysNothing(annotate(report.command("create"), {
78
82
  effects: LOCAL_WRITE,
79
- examples: [`${app.command} personal doctor report create --output ./problem-report.json`],
83
+ examples: [`${app.command} personal doctor reports create --output ./problem-report.json`],
80
84
  }))
81
85
  .description("write a problem report to a file, and say where to send it")
82
86
  .option("--run <id>", "the run the report is about; the newest failed one if not given")
@@ -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")