@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
@@ -121,22 +121,22 @@ export const chatsCommand = (messenger) => {
121
121
  inspect: "https://example.com/invite/book-club",
122
122
  join: "https://example.com/invite/book-club",
123
123
  leave: '"Book club"',
124
- "link create": '"Book club"',
125
- "link list": '"Book club"',
126
- "link reset": '"Book club"',
127
- "link revoke": '"Book club" https://example.com/invite/book-club',
128
- "link show": '"Book club"',
124
+ "links create": '"Book club"',
125
+ "links list": '"Book club"',
126
+ "links reset": '"Book club"',
127
+ "links remove": '"Book club" https://example.com/invite/book-club',
128
+ "links show": '"Book club"',
129
129
  list: "--limit 10",
130
130
  "mark-read": '"Book club" --until-id 101',
131
131
  "members add": '"Book club" "Alex Example"',
132
- "members audit": '"Book club"',
132
+ "members check": '"Book club"',
133
133
  "members fetch": '"Book club"',
134
134
  "members history": '"Book club"',
135
135
  "members list": '"Book club"',
136
136
  "members remove": '"Book club" "Alex Example"',
137
137
  moderate: '"Book club" --dry-run',
138
- "requests accept": '"Book club" "Alex Example"',
139
- "requests decline": '"Book club" "Alex Example"',
138
+ "requests confirm": '"Book club" "Alex Example"',
139
+ "requests reject": '"Book club" "Alex Example"',
140
140
  "requests list": '"Book club"',
141
141
  "rules set": '"Book club" links report',
142
142
  "rules show": '"Book club"',
@@ -23,7 +23,7 @@ export const membersCommand = (messenger) => {
23
23
  const body = await context.withServices((services) => membersList(services, chat, context.settings));
24
24
  renderAnswer(this, context, body, lines(({ id, name, username, role }) => [id, name ?? "", username ? `@${username}` : "", role ?? ""].join(" ")));
25
25
  }));
26
- members.addCommand(withQuery(annotate(annotate(new Command("audit"), { effects: REMOTE_READ, offline: false })
26
+ members.addCommand(withQuery(annotate(annotate(new Command("check"), { effects: REMOTE_READ, offline: false })
27
27
  .summary("members that look like bots, with reasons")
28
28
  .description("members that look like bots, each with its reasons — read from the member list and the local store; " +
29
29
  "never one request per person, and it removes nobody")
@@ -69,7 +69,7 @@ export const contactsCommand = (messenger) => {
69
69
  }));
70
70
  renderAnswer(this, context, itemOf(contactAnswer(account, card)));
71
71
  }));
72
- annotate(contacts.command("profile"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
72
+ annotate(contacts.command("info"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
73
73
  .summary("a person's profile from the messenger")
74
74
  .description("everything the messenger says about one person — handles, flags, last seen, when they registered — and " +
75
75
  "how many of their messages the store holds in each chat you share, the first and the last, and the " +
@@ -173,7 +173,10 @@ export const contactsCommand = (messenger) => {
173
173
  meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
174
174
  }));
175
175
  });
176
- annotate(contacts.command("timeline"), { effects: REMOTE_READ, output: { entity: "contact_activity" } })
176
+ annotate(contacts.command("events"), {
177
+ effects: REMOTE_READ,
178
+ output: { entity: "contact_activity" },
179
+ })
177
180
  .summary("one person's activity across their linked identities")
178
181
  .description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
179
182
  "mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
@@ -200,7 +203,7 @@ export const contactsCommand = (messenger) => {
200
203
  ...(cursor === undefined ? {} : { cursor }),
201
204
  }));
202
205
  const { items, hasMore, limits, pending } = found;
203
- renderAnswer(this, context, listOf("contacts timeline", {
206
+ renderAnswer(this, context, listOf("contacts events", {
204
207
  items: items.map(snakeKeys),
205
208
  limit: limits.items,
206
209
  has_more: hasMore,
@@ -292,8 +295,8 @@ export const contactsCommand = (messenger) => {
292
295
  contacts.addCommand(command);
293
296
  const examples = {
294
297
  add: '"Alex Example"',
295
- "alias rm": '"Alex Example"',
296
- "alias set": '"Alex Example" "Reading partner"',
298
+ "aliases remove": '"Alex Example"',
299
+ "aliases set": '"Alex Example" "Reading partner"',
297
300
  block: '"Alex Example"',
298
301
  check: '"Alex Example"',
299
302
  context: '"Alex Example" --limit 10',
@@ -306,12 +309,12 @@ export const contactsCommand = (messenger) => {
306
309
  "notes list": '"Alex Example"',
307
310
  "notes remove": '"Alex Example" 1',
308
311
  "notes show": '"Alex Example" 1',
309
- profile: '"Alex Example"',
312
+ info: '"Alex Example"',
310
313
  remove: '"Alex Example"',
311
314
  rename: '"Alex Example" Alex Example',
312
315
  show: '"Alex Example"',
313
316
  sync: "",
314
- timeline: '"Alex Example" --limit 10',
317
+ events: '"Alex Example" --limit 10',
315
318
  unblock: '"Alex Example"',
316
319
  unlink: '"Alex Example"',
317
320
  };
@@ -1,5 +1,5 @@
1
1
  import type { Command } from "commander";
2
- import type { AdminRight, GroupSettingKey, Id, Message, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
2
+ import type { AdminRight, GroupSettingKey, Id, Message, Page, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
3
3
  import type { MessengerAdapter } from "../../ports/messenger.js";
4
4
  import type { EventSink, Warn } from "../../runs/events.js";
5
5
  import { type Asker, type SendGuard } from "../../sends/guard.js";
@@ -18,6 +18,11 @@ import type { GlobalFlags, ResolveOptions, Settings } from "../settings.js";
18
18
  */
19
19
  export interface StoredMessages {
20
20
  message(chatId: Id, messageId: Id): Promise<Message | undefined>;
21
+ /** Stored messages in one provider thread, oldest first within the newest page. No remote fetch. */
22
+ threadMessages?(chatId: Id, threadId: Id, window: {
23
+ limit: number;
24
+ before?: Id;
25
+ }): Promise<Page<Message>>;
21
26
  /** The newest message from someone else in the chat, older than `before` when it is given. */
22
27
  newestIncoming(chatId: Id, before?: Id): Promise<Message | undefined>;
23
28
  /** The newest message in the chat sent at `at` or earlier; without `at`, the oldest one held. */
@@ -148,20 +153,22 @@ export interface Messenger extends ServiceMessenger {
148
153
  stickerFileBytes?: number;
149
154
  /** Whether shared folders can be joined; yes when unset. */
150
155
  folderJoin?: boolean;
151
- /** Whether another invite link can be made — `chats link create`; yes when unset. */
156
+ /** Whether another invite link can be made — `chats links create`; yes when unset. */
152
157
  inviteLinkCreate?: boolean;
153
158
  /** Whether invite links can be listed; yes when unset. */
154
159
  inviteLinkList?: boolean;
155
160
  /** Whether one invite link can be revoked; yes when unset. */
156
161
  inviteLinkRevoke?: boolean;
157
- /** Whether one invite link can be changed — `chats link update`; no when unset. */
162
+ /** Whether one invite link can be changed — `chats links update`; no when unset. */
158
163
  inviteLinkUpdate?: boolean;
159
- /** Whether every pending join request is answered at once — `chats requests accept --all`; yes when unset. */
164
+ /** Whether every pending join request is answered at once — `chats requests confirm --all`; yes when unset. */
160
165
  joinRequestsAll?: boolean;
161
166
  /** Whether `--all` can take only the requests made by one invite link — `--link`; yes when unset. */
162
167
  joinRequestsByLink?: boolean;
163
168
  /** Whether a channel post can be commented on — `messages send --comment-to`; yes when unset. */
164
169
  channelComments?: boolean;
170
+ /** Whether comments can be read independently of sending; inherits channelComments when unset. */
171
+ readsComments?: boolean;
165
172
  /** Whether people added can be shown the messages from before they came — `members add --history`; yes when unset. */
166
173
  addsWithHistory?: boolean;
167
174
  /** The admin rights this messenger has, as `admins add --can` offers them; every one when unset. */
@@ -40,6 +40,12 @@ export const storedMessages = (provider, store, account) => {
40
40
  const at = key();
41
41
  return at && (await store()).messages.message(at, messageId, { chatId });
42
42
  },
43
+ threadMessages: async (chatId, threadId, window) => {
44
+ const owner = key();
45
+ return owner
46
+ ? (await store()).messages.messages(owner, chatId, { ...window, threadId })
47
+ : { items: [], hasMore: false };
48
+ },
43
49
  newestIncoming: async (chatId, before) => {
44
50
  const at = key();
45
51
  if (!at)
@@ -2,13 +2,14 @@ import { annotate } from "@wirecat/cli-base/commands";
2
2
  import { commandAt } from "../steps.js";
3
3
  export const conversationExamples = (root, tool) => {
4
4
  const examples = {
5
- "batches next": '--chat "Book club"',
5
+ "batches show": '--chat "Book club"',
6
6
  "batches status": '--chat "Book club"',
7
- build: '--chat "Book club"',
7
+ "links rebuild": '--chat "Book club"',
8
8
  "consents list": "",
9
- "consents revoke": '--chat "Book club"',
10
- "embed clear": '--chat "Book club"',
11
- "embed status": '--chat "Book club"',
9
+ "consents remove": '--chat "Book club"',
10
+ embed: '--chat "Book club"',
11
+ "embeddings clear": '--chat "Book club"',
12
+ "embeddings status": '--chat "Book club"',
12
13
  "links add": '--batch "$batch"',
13
14
  "links clear": '--chat "Book club"',
14
15
  list: '"Book club"',
@@ -21,5 +22,5 @@ export const conversationExamples = (root, tool) => {
21
22
  if (command)
22
23
  annotate(command, { examples: [`${tool} personal conversations ${path} ${args}`.trimEnd()] });
23
24
  }
24
- commandAt(root, "links add")?.addHelpText("after", "\nSet batch to the id from conversations batches next; supply the JSON answer to that batch on stdin.\n");
25
+ commandAt(root, "links add")?.addHelpText("after", "\nSet batch to the id from conversations batches show; supply the JSON answer to that batch on stdin.\n");
25
26
  };
@@ -1,9 +1,5 @@
1
1
  import { Command } from "commander";
2
2
  import { type Messenger } from "./context.js";
3
- /**
4
- * `conversations`: the threads inside a group chat, found by rules over the stored messages — replies,
5
- * mentions, one sender's messages in a row. From the store alone; nothing is built until asked (phase 3).
6
- */
7
3
  export declare const conversationsCommand: (messenger: Messenger) => Command;
8
4
  /** `messages links`: why a message sits where it does in its conversation. */
9
5
  export declare const linksCommand: (messenger: Messenger) => Command;
@@ -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")