@wirecat/cli-core 0.346.0 → 0.348.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 (69) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/cli/bot/copy.d.ts +1 -1
  3. package/dist/cli/bot/copy.js +1 -1
  4. package/dist/cli/embedding-choice.js +2 -1
  5. package/dist/cli/envelope.d.ts +4 -0
  6. package/dist/cli/envelope.js +4 -3
  7. package/dist/cli/guide.js +4 -0
  8. package/dist/cli/help/first-screen.js +13 -1
  9. package/dist/cli/help/sections.d.ts +4 -3
  10. package/dist/cli/help/sections.js +5 -10
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.js +1 -0
  13. package/dist/cli/messenger/backfill-command.js +6 -6
  14. package/dist/cli/messenger/context.d.ts +3 -3
  15. package/dist/cli/messenger/context.js +44 -17
  16. package/dist/cli/messenger/doctor-command.js +3 -3
  17. package/dist/cli/messenger/flooded.js +1 -1
  18. package/dist/cli/messenger/mcp-command.js +1 -1
  19. package/dist/cli/messenger/messages-delete-command.js +2 -1
  20. package/dist/cli/messenger/observed.js +21 -7
  21. package/dist/cli/messenger/schedule-command.js +4 -3
  22. package/dist/cli/messenger/serve-relay.js +1 -0
  23. package/dist/cli/messenger/store-maintenance-command.js +3 -3
  24. package/dist/cli/messenger/stored.js +36 -30
  25. package/dist/cli/program.js +10 -9
  26. package/dist/cli/runs/command.js +3 -2
  27. package/dist/cli/steps.d.ts +4 -0
  28. package/dist/cli/steps.js +9 -0
  29. package/dist/cli/store-settings.js +4 -3
  30. package/dist/domain/models.d.ts +11 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/kit/contract.d.ts +2 -2
  34. package/dist/kit/contract.js +25 -0
  35. package/dist/kit/fake.d.ts +1 -0
  36. package/dist/kit/fake.js +15 -16
  37. package/dist/mcp/meeting-preparation.d.ts +1 -0
  38. package/dist/mcp/meeting-project-candidates.d.ts +1 -0
  39. package/dist/mcp/meeting-series-inventory.d.ts +1 -0
  40. package/dist/mcp/tool.js +2 -0
  41. package/dist/mcp/tools/messages-photo.js +2 -1
  42. package/dist/meetings/cli/query-options.js +5 -1
  43. package/dist/meetings/mcp/passage.d.ts +1 -0
  44. package/dist/meetings/mcp/project-evidence.d.ts +1 -0
  45. package/dist/meetings/mcp/search.d.ts +1 -0
  46. package/dist/meetings/reads.d.ts +1 -0
  47. package/dist/ports/messenger.d.ts +10 -2
  48. package/dist/runs/recording.js +1 -0
  49. package/dist/runs/scope.d.ts +7 -0
  50. package/dist/runs/scope.js +5 -0
  51. package/dist/sends/permissions.js +2 -0
  52. package/dist/services/archive.js +2 -1
  53. package/dist/services/chats.js +3 -2
  54. package/dist/services/conversations.js +2 -1
  55. package/dist/services/deps.js +2 -1
  56. package/dist/services/embeddings-maintenance.js +3 -2
  57. package/dist/services/index.d.ts +1 -1
  58. package/dist/services/search-all-meetings.d.ts +3 -4
  59. package/dist/services/search-all.d.ts +8 -1
  60. package/dist/services/search-all.js +1 -1
  61. package/dist/speech/transcribe.js +2 -1
  62. package/dist/store/chats.d.ts +2 -2
  63. package/dist/store/sqlite/chats.d.ts +2 -2
  64. package/dist/store/sqlite/chats.js +7 -7
  65. package/dist/store/types.d.ts +2 -2
  66. package/dist/terminal/login-qr.d.ts +27 -0
  67. package/dist/terminal/login-qr.js +44 -0
  68. package/package.json +3 -3
  69. package/parity.json +14 -2
@@ -21,6 +21,11 @@ export const currentRun = () => {
21
21
  return processScope;
22
22
  };
23
23
  export const inRun = (scope, body) => scopes.run(scope, body);
24
+ /**
25
+ * A command as a person types it: `max personal store migrate`. A hint without the profile fails when copied,
26
+ * since no profile is assumed; outside a run with a profile it stays `max store migrate`.
27
+ */
28
+ export const typedCommand = (tool, words, profile = currentRun().profile) => [tool, profile, words].filter((part) => part !== undefined && part !== "").join(" ");
24
29
  /**
25
30
  * For a one-shot command whose action runs outside `inRun` — max's `max bot`, recorded from commander's hooks, which
26
31
  * cannot wrap the action — so its answers' `meta.run_id` names the run `runs show` finds. Never in a server.
@@ -341,6 +341,8 @@ export const WRITE_KEYS = [
341
341
  ];
342
342
  /** Commands that look after the tool and its files and never show a message: no level stops them. */
343
343
  const HOUSEKEEPING = new Set([
344
+ "help",
345
+ "guide",
344
346
  "config",
345
347
  "session",
346
348
  "doctor",
@@ -1,6 +1,7 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
2
  import { CliError } from "@wirecat/cli-base";
3
3
  import { capability } from "../ports/messenger.js";
4
+ import { typedCommand } from "../runs/scope.js";
4
5
  import { fetchedKey, historyStartKey, } from "../store/store.js";
5
6
  import { estimateBackfill } from "./backfill-estimate.js";
6
7
  import { actionable, batchProgress } from "./batch.js";
@@ -11,7 +12,7 @@ import { catchUpSearch, validateCatchUp } from "./search-catchup.js";
11
12
  const pushed = (deps, what) => {
12
13
  if (deps.reads === "store") {
13
14
  throw new CliError("validation_error", `${what} asks for older history, and this messenger pushes its history instead — keep ` +
14
- `\`${deps.messenger.app.command} serve\` running`);
15
+ `\`${typedCommand(deps.messenger.app.command, "serve")}\` running`);
15
16
  }
16
17
  };
17
18
  /** The most messages a provider hands out per history request — Telegram's cap. */
@@ -4,6 +4,7 @@ import { botCheckService } from "../botcheck/service.js";
4
4
  import { threadIdOf } from "../domain/thread.js";
5
5
  import { capability } from "../ports/messenger.js";
6
6
  import { syncsOneChat } from "../ports/traits.js";
7
+ import { typedCommand } from "../runs/scope.js";
7
8
  import { codeOf, guardedWrite } from "../sends/guarded.js";
8
9
  import { newOperationId } from "../sends/send-id.js";
9
10
  import { CHAT_COLUMNS, queryFilter, } from "../store/store.js";
@@ -147,7 +148,7 @@ export const chatsService = (deps) => ({
147
148
  const counted = counts.length > 0 ? { ...known, memberCounts: counts } : known;
148
149
  return completeness.state === "complete" || !syncsOneChat(deps.messenger)
149
150
  ? counted
150
- : { ...counted, fetch: `${deps.messenger.app.command} messages sync ${chatId}` };
151
+ : { ...counted, fetch: typedCommand(deps.messenger.app.command, `messages sync ${chatId}`) };
151
152
  },
152
153
  sendAs: async (chat) => {
153
154
  if (deps.offline) {
@@ -180,7 +181,7 @@ export const chatsService = (deps) => ({
180
181
  unknown,
181
182
  ...(stored?.completeness ? { completeness: stored.completeness } : {}),
182
183
  ...(stored?.completeness && stored.completeness.state !== "complete" && syncsOneChat(deps.messenger)
183
- ? { fetch: `${deps.messenger.app.command} messages sync ${id}` }
184
+ ? { fetch: typedCommand(deps.messenger.app.command, `messages sync ${id}`) }
184
185
  : {}),
185
186
  items,
186
187
  };
@@ -5,6 +5,7 @@ import { linkMessages, RULES_VERSION } from "../conversations/link.js";
5
5
  import { formatLocator } from "../domain/locator.js";
6
6
  import { formatReference } from "../domain/references.js";
7
7
  import { conversation } from "../entities/conversation.js";
8
+ import { typedCommand } from "../runs/scope.js";
8
9
  import { CONVERSATION_COLUMNS, queryFilter, } from "../store/store.js";
9
10
  import { accountStore } from "./deps.js";
10
11
  import { chatIdIn } from "./message-resolution.js";
@@ -38,7 +39,7 @@ export const conversationsService = (deps) => {
38
39
  const store = await accountStore(deps);
39
40
  return { store, chatId: await chatIdIn(deps.messenger, chat, store) };
40
41
  };
41
- const notBuilt = (chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${deps.messenger.app.command} conversations build --chat ${chatId}\``);
42
+ const notBuilt = (chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations build --chat ${chatId}`)}\``);
42
43
  return {
43
44
  build: async (chat, { maxMessages, check } = {}) => {
44
45
  check?.();
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { isCliFailure } from "../errors/failures.js";
3
+ import { typedCommand } from "../runs/scope.js";
3
4
  /** The store with the running account bound: the default for a service's reads and writes. */
4
5
  export const accountStore = async (deps) => (await deps.store()).forAccount(await deps.account());
5
6
  /**
@@ -49,7 +50,7 @@ export const storeIfOpen = async (deps) => {
49
50
  /** Whether `chats`, `messages list|context` and `contacts` answer from the store instead of the messenger. */
50
51
  export const fromStore = (deps) => deps.offline || deps.reads === "store";
51
52
  /** What a store-mode read answers for a chat the store holds nothing of. */
52
- export const nothingStored = (messenger) => `nothing stored for this chat yet — keep \`${messenger.app.command} serve\` running`;
53
+ export const nothingStored = (messenger) => `nothing stored for this chat yet — keep \`${typedCommand(messenger.app.command, "serve")}\` running`;
53
54
  /** Where a store-mode messenger's history comes from, for what it cannot do. */
54
55
  export const PUSHED = "this messenger's history is read from the local store";
55
56
  export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context` and `contacts list|show`";
@@ -6,6 +6,7 @@ import { DEFAULT_TEXT_MODEL, textModel, vectorModelKey } from "../embeddings/mod
6
6
  import { remoteKey } from "../embeddings/remote.js";
7
7
  import { CONTEXT_MESSAGES, CONTEXT_MINUTES } from "../images/context/index.js";
8
8
  import { registeredImageProviders } from "../images/registry.js";
9
+ import { typedCommand } from "../runs/scope.js";
9
10
  import { conversationsService } from "./conversations.js";
10
11
  import { storedChatId } from "./message-resolution.js";
11
12
  /** How much one run without `--chat` does at most, by default. */
@@ -16,11 +17,11 @@ const PER_SESSION = 8;
16
17
  const PER_REQUEST = 256;
17
18
  /** e5's tokenizer reads ~3.7 characters a token in Russian; 3 keeps the bound above the real count. */
18
19
  const CHARS_PER_TOKEN_AT_LEAST = 3;
19
- const notDownloaded = (deps, id) => new CliError("not_found", `${id} is not downloaded — \`${deps.messenger.app.command} models text download ${id}\``, {
20
+ const notDownloaded = (deps, id) => new CliError("not_found", `${id} is not downloaded — \`${typedCommand(deps.messenger.app.command, `models text download ${id}`)}\``, {
20
21
  reason: "model.not-downloaded",
21
22
  model: id,
22
23
  });
23
- export const notBuiltError = (deps, chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${deps.messenger.app.command} conversations build --chat ${chatId}\``);
24
+ export const notBuiltError = (deps, chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations build --chat ${chatId}`)}\``);
24
25
  export const embeddingTarget = (deps, choice = DEFAULT_TEXT_MODEL) => {
25
26
  if (typeof choice !== "string") {
26
27
  const { remote, apiKey, concurrency = 4 } = choice;
@@ -156,7 +156,7 @@ export { privatePeopleService } from "./private-people.js";
156
156
  export { approveAsTask, type ProposalTaskStore, proposalSource } from "./proposal-tasks.js";
157
157
  export type { RetentionQuery, RetentionService } from "./retention.js";
158
158
  export { retentionService } from "./retention.js";
159
- export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
159
+ export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
160
160
  export { type MeetingSearchCoverage, type MeetingSearchCursor, type MeetingSearchItem, RESOURCES_SEARCHED_WITH_MEETINGS, type ResourceWithMeetings, type SearchAllIncludingMeetingsFound, type SearchAllIncludingMeetingsRequest, type SearchAllWithMeetingsRequest, searchAllIncludingMeetings, searchAllWithMeetings, } from "./search-all-meetings.js";
161
161
  export { CATCH_UP_BOUNDS, type CatchUpOptions, type CatchUpResult, catchUpSearch, validateCatchUp, } from "./search-catchup.js";
162
162
  export { accountsOfKind, listContextOf, type SearchKind } from "./search-kind.js";
@@ -1,8 +1,7 @@
1
1
  import type { SearchHit } from "../meetings/index.js";
2
- import type { MessengerTraits } from "../ports/traits.js";
3
2
  import type { AccountKey, MessageStore } from "../store/store.js";
4
3
  import type { SearchFound, SearchQuery } from "./messages.js";
5
- import { type SearchAllFound, type SearchAllItem, type SearchAllRequest, type Skipped } from "./search-all.js";
4
+ import { type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type Skipped } from "./search-all.js";
6
5
  export declare const RESOURCES_SEARCHED_WITH_MEETINGS: readonly ["messages", "mail", "notes", "meetings"];
7
6
  export type ResourceWithMeetings = (typeof RESOURCES_SEARCHED_WITH_MEETINGS)[number];
8
7
  export interface MeetingSearchItem {
@@ -55,7 +54,7 @@ export interface SearchAllIncludingMeetingsFound extends Omit<SearchAllFound, "i
55
54
  meetings?: MeetingSearchCoverage;
56
55
  }
57
56
  /** Opt-in fusion of the existing unified ranking and one explicitly scoped meeting archive. */
58
- export declare const searchAllIncludingMeetings: (store: MessageStore, account: AccountKey, request: SearchAllIncludingMeetingsRequest, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app">>, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllIncludingMeetingsFound>;
57
+ export declare const searchAllIncludingMeetings: (store: MessageStore, account: AccountKey, request: SearchAllIncludingMeetingsRequest, messenger?: SearchAllMessenger, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllIncludingMeetingsFound>;
59
58
  export interface SearchAllWithMeetingsRequest extends Omit<SearchAllIncludingMeetingsRequest, "meetingAccount"> {
60
59
  /**
61
60
  * `--source`: each kind reads the accounts the list names of it and a kind it names none of is skipped.
@@ -66,4 +65,4 @@ export interface SearchAllWithMeetingsRequest extends Omit<SearchAllIncludingMee
66
65
  /** Meetings have no index to search every account at once, so they are read only when named. */
67
66
  export declare const MEETINGS_UNNAMED = "meetings are searched only when --source names a meeting account, zoom or meetings";
68
67
  /** `search all`: every kind but meetings, unless `--source` names a meeting account. */
69
- export declare const searchAllWithMeetings: (store: MessageStore, account: AccountKey, request: SearchAllWithMeetingsRequest, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app">>, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllIncludingMeetingsFound>;
68
+ export declare const searchAllWithMeetings: (store: MessageStore, account: AccountKey, request: SearchAllWithMeetingsRequest, messenger?: SearchAllMessenger, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllIncludingMeetingsFound>;
@@ -81,9 +81,16 @@ export interface SearchAllRequest {
81
81
  /** The accounts `--source` chose, per kind; a kind left out reads every account of its own. */
82
82
  scope?: Partial<Record<SearchedResource, AccountKey[]>>;
83
83
  }
84
+ export type SearchAllMessenger = Partial<Pick<MessengerTraits, "savedChatId" | "app">> & {
85
+ /**
86
+ * The tool named in a model-download hint, when it is not `app.command`: memo passes tg's identity for `group:`
87
+ * lists but has no `models` command. `null` names the tools that have one.
88
+ */
89
+ hintCommand?: string | null;
90
+ };
84
91
  /**
85
92
  * `search all`: messages, mail and notes from the local store — messages also from the messenger's
86
93
  * server, as `search messages` asks it — each list best first, merged by reciprocal rank so no
87
94
  * resource's own scores have to be compared with another's.
88
95
  */
89
- export declare const searchAll: (store: MessageStore, account: AccountKey, request: SearchAllRequest, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app">>, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllFound>;
96
+ export declare const searchAll: (store: MessageStore, account: AccountKey, request: SearchAllRequest, messenger?: SearchAllMessenger, searchMessages?: (query: SearchQuery) => Promise<SearchFound>) => Promise<SearchAllFound>;
@@ -21,7 +21,7 @@ export const searchAll = async (store, account, request, messenger = {}, searchM
21
21
  // Each resource is read deep enough for the merged page, then the merge is cut.
22
22
  const window = offset + request.limit;
23
23
  const only = request.only ?? RESOURCES_SEARCHED;
24
- const command = messenger.app?.command;
24
+ const command = messenger.hintCommand === undefined ? messenger.app?.command : (messenger.hintCommand ?? undefined);
25
25
  const lists = [];
26
26
  const searched = [];
27
27
  const accounts = [];
@@ -1,6 +1,7 @@
1
1
  import { CliError, isCliError } from "@wirecat/cli-base";
2
2
  import { fromFile } from "../cli/settings.js";
3
3
  import { capability } from "../ports/messenger.js";
4
+ import { typedCommand } from "../runs/scope.js";
4
5
  import { installedBytes, isInstalled, megabytes, modelPath, modelsDirectory, vadPath } from "./install.js";
5
6
  import { orderedModels, speechModel } from "./models.js";
6
7
  /** `messenger`: its own recognition (Telegram Premium). `local`: a model on this machine. `auto`: the first, then the second. */
@@ -20,7 +21,7 @@ export const choose = (messenger, settings, flags, env = process.env) => {
20
21
  };
21
22
  /** Never a download: a model is hundreds of MB, and whether to fetch one is the owner's call. */
22
23
  export const notDownloaded = (messenger, model) => new CliError("not_found", `the speech model ${model.id} is not on this machine — the owner downloads it once with ` +
23
- `\`${messenger.app.command} models audio download ${model.id}\` (${megabytes(installedBytes(model))})`, { reason: "model.not-downloaded", model: model.id });
24
+ `\`${typedCommand(messenger.app.command, `models audio download ${model.id}`)}\` (${megabytes(installedBytes(model))})`, { reason: "model.not-downloaded", model: model.id });
24
25
  /**
25
26
  * Over an open connection: the messenger's own transcript, or the recording's bytes for a local model.
26
27
  * The model runs after the connection closes — a minute of speech should not hold a socket open.
@@ -1,5 +1,5 @@
1
1
  import type { ChannelTagMatch } from "../domain/channel-tags.js";
2
- import type { Chat, ChatKind, Id, Page } from "../domain/models.js";
2
+ import type { Chat, ChatKind, ChatUpdate, Id, Page } from "../domain/models.js";
3
3
  import { type ChatRekey } from "./sqlite/chat-rekey.js";
4
4
  import type { ChatMetadata } from "./sqlite/chats.js";
5
5
  import type { ChatCompleteness } from "./sqlite/completeness.js";
@@ -7,7 +7,7 @@ import type { StoreContext } from "./sqlite/open.js";
7
7
  import type { Where } from "./sqlite/query-filter.js";
8
8
  import type { AccountKey, ChatActivity, ChatStats, StoredChatFilter } from "./types.js";
9
9
  export interface ChatsStore {
10
- saveChats(key: AccountKey, chats: Chat[]): Promise<void>;
10
+ saveChats(key: AccountKey, chats: ChatUpdate[]): Promise<void>;
11
11
  /** Newest first. `query` matches three letters or more of a title; `unread` keeps chats with unread messages. */
12
12
  chats(key: AccountKey, window: {
13
13
  limit?: number;
@@ -1,5 +1,5 @@
1
1
  import type { ChannelTagMatch } from "../../domain/channel-tags.js";
2
- import type { Chat, Id, Member, Page } from "../../domain/models.js";
2
+ import type { Chat, ChatUpdate, Id, Member, Page } from "../../domain/models.js";
3
3
  import type { AccountKey, StoredChatFilter } from "../types.js";
4
4
  import type { StoreContext } from "./open.js";
5
5
  /** The chat's own id, when the store key is a chat of this account. */
@@ -7,7 +7,7 @@ export declare const chatOf: ({ orm }: StoreContext, accountKey: number, chatKey
7
7
  export declare const findChatPk: ({ orm }: StoreContext, accountKey: number, chatId: Id) => number | undefined;
8
8
  /** A chat known only from its messages, until the chat list names it. */
9
9
  export declare const chatPkFor: (context: StoreContext, accountKey: number, chatId: Id) => number;
10
- export declare const upsertChat: (context: StoreContext, accountKey: number, chat: Chat) => void;
10
+ export declare const upsertChat: (context: StoreContext, accountKey: number, chat: ChatUpdate) => void;
11
11
  /** Replaces who is in the chat, whole. */
12
12
  export declare const writeMembers: (context: StoreContext, key: AccountKey, accountKey: number, chatId: Id, memberIds: Id[]) => void;
13
13
  export declare const members: ({ orm }: StoreContext, chatKey: number) => Member[];
@@ -45,10 +45,10 @@ export const upsertChat = (context, accountKey, chat) => {
45
45
  kind: chat.kind,
46
46
  parentChatId: parent,
47
47
  scope: chat.scope ?? null,
48
- title: chat.title,
49
- unreadCount: chat.unreadCount,
48
+ title: chat.title ?? null,
49
+ unreadCount: chat.unreadCount ?? null,
50
50
  lastMessageAt: toMs(chat.lastMessageAt),
51
- participantsCount: chat.participantsCount,
51
+ participantsCount: chat.participantsCount ?? null,
52
52
  metadata: mergedMetadata(stored?.metadata ?? null, chat.providerMetadata),
53
53
  membershipState: chat.membershipState ?? null,
54
54
  createdAt: now(),
@@ -60,10 +60,10 @@ export const upsertChat = (context, accountKey, chat) => {
60
60
  kind: sql `excluded.kind`,
61
61
  ...(chat.parentChatId === undefined ? {} : { parentChatId: sql `excluded.parent_chat_id` }),
62
62
  scope: sql `coalesce(excluded.scope, ${chats.scope})`,
63
- title: sql `excluded.title`,
64
- unreadCount: sql `excluded.unread_count`,
65
- lastMessageAt: sql `excluded.last_message_at`,
66
- participantsCount: sql `excluded.participants_count`,
63
+ ...(chat.title === undefined ? {} : { title: sql `excluded.title` }),
64
+ ...(chat.unreadCount === undefined ? {} : { unreadCount: sql `excluded.unread_count` }),
65
+ ...(chat.lastMessageAt === undefined ? {} : { lastMessageAt: sql `excluded.last_message_at` }),
66
+ ...(chat.participantsCount === undefined ? {} : { participantsCount: sql `excluded.participants_count` }),
67
67
  metadata: sql `excluded.metadata`,
68
68
  // A chat the list names again has been rejoined; any other state it had is kept.
69
69
  membershipState: sql `CASE WHEN excluded.membership_state IS NOT NULL THEN excluded.membership_state
@@ -1,6 +1,6 @@
1
1
  import type { TextRange } from "../conversations/chunks.js";
2
2
  import type { Link } from "../conversations/link.js";
3
- import type { Chat, ChatKind, Id, MessageHit, Provider } from "../domain/models.js";
3
+ import type { Chat, ChatKind, ChatUpdate, Id, MessageHit, Provider } from "../domain/models.js";
4
4
  import type * as imageChunks from "./sqlite/image-chunks.js";
5
5
  import type { Where } from "./sqlite/query-filter.js";
6
6
  export type PictureInput = imageChunks.PictureInput;
@@ -82,7 +82,7 @@ export type MessageFilter = AccountMessageFilter & {
82
82
  chatId?: undefined;
83
83
  });
84
84
  export interface Delta {
85
- chats?: Chat[];
85
+ chats?: ChatUpdate[];
86
86
  people?: PersonFacts[];
87
87
  members?: Map<Id, Id[]>;
88
88
  state?: Record<string, string>;
@@ -0,0 +1,27 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ /** Whether a person can scan a code drawn here and answer what the login asks next. */
3
+ export declare const atTerminal: (input: NodeJS.ReadableStream & {
4
+ isTTY?: boolean;
5
+ }) => boolean;
6
+ /** The one refusal of a QR login that nobody could scan. */
7
+ export declare const qrNeedsTerminal: (command: string) => CliError;
8
+ export interface LoginQrOptions {
9
+ /** `--qr-file`: the PNG to write instead of drawing the code. */
10
+ qrFile?: string;
11
+ /** Where the code and the narrow-terminal hint go; `--quiet` must not hide either. */
12
+ diagnostic: (text: string) => void;
13
+ /** Where the file's location is said. */
14
+ note: (text: string) => void;
15
+ /** How far the code sits right, to line up under a setup step. */
16
+ indent?: number;
17
+ /** Read on every code, so a widened terminal draws the next one. Unknown draws the code. */
18
+ columns?: () => number | undefined;
19
+ }
20
+ export interface LoginQr {
21
+ readonly path: string | undefined;
22
+ /** Never throws for a code that does not fit: the messengers call it from inside their own event loops. */
23
+ show: (link: string) => void;
24
+ /** The image logs the account in while it is valid; call this in `finally`. */
25
+ close: () => void;
26
+ }
27
+ export declare const loginQr: ({ qrFile, diagnostic, note, indent: pad, columns, }: LoginQrOptions) => LoginQr;
@@ -0,0 +1,44 @@
1
+ import { rmSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { CliError, indent, writeSecurely } from "@wirecat/cli-base";
4
+ import { inputPolicy } from "../cli/input-policy.js";
5
+ import { qrPng, terminalQr } from "./qr.js";
6
+ const MARGIN = 2;
7
+ /** Whether a person can scan a code drawn here and answer what the login asks next. */
8
+ export const atTerminal = (input) => input.isTTY === true && !inputPolicy(input).noInput;
9
+ /** The one refusal of a QR login that nobody could scan. */
10
+ export const qrNeedsTerminal = (command) => new CliError("validation_error", `${command} shows a QR code to scan — run it in a terminal, or pass --qr-file <png>`, {
11
+ param: "--qr-file",
12
+ reason: "input.required",
13
+ });
14
+ export const loginQr = ({ qrFile, diagnostic, note, indent: pad = 0, columns = () => process.stderr.columns, }) => {
15
+ const path = qrFile === undefined ? undefined : resolve(qrFile);
16
+ // Only what this login wrote is removed: a refused or never-reached write leaves the owner's file alone.
17
+ let written = false;
18
+ return {
19
+ path,
20
+ show: (link) => {
21
+ if (path) {
22
+ writeSecurely(path, qrPng(link), 0o600);
23
+ written = true;
24
+ note(`the QR code is in ${path} — it is replaced when the code renews`);
25
+ return;
26
+ }
27
+ const drawn = terminalQr(link);
28
+ const need = drawn.width + pad + MARGIN;
29
+ const width = columns();
30
+ if (width !== undefined && need > width) {
31
+ diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it, ` +
32
+ "or run again with --qr-file <png>");
33
+ return;
34
+ }
35
+ diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
36
+ },
37
+ close: () => {
38
+ if (!path || !written)
39
+ return;
40
+ written = false;
41
+ rmSync(path, { force: true });
42
+ },
43
+ };
44
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.346.0",
3
+ "version": "0.348.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -202,7 +202,7 @@
202
202
  "@types/node": "^22.20.4",
203
203
  "@types/pngjs": "6.0.5",
204
204
  "@vitest/coverage-v8": "^5.0.2",
205
- "@wirecat/cli-base": "0.46.0",
205
+ "@wirecat/cli-base": "0.47.0",
206
206
  "commander": "15.0.0",
207
207
  "cspell": "10.3.6",
208
208
  "drizzle-kit": "1.0.0-rc.4",
@@ -222,7 +222,7 @@
222
222
  },
223
223
  "peerDependencies": {
224
224
  "@napi-rs/canvas": ">=0.1.69 <2",
225
- "@wirecat/cli-base": ">=0.45.0 <0.47.0",
225
+ "@wirecat/cli-base": ">=0.47.0 <0.48.0",
226
226
  "commander": ">=15"
227
227
  },
228
228
  "bin": {
package/parity.json CHANGED
@@ -2847,6 +2847,14 @@
2847
2847
  "wa": "wa backlog: mount the shared flood command"
2848
2848
  }
2849
2849
  },
2850
+ "guide": {
2851
+ "in": [],
2852
+ "planned": {
2853
+ "max": "help-navigation pass: max-cli#737 mounts guide with scenarios",
2854
+ "tg": "help-navigation pass: mount guide with scenarios",
2855
+ "wa": "help-navigation pass: mount guide with scenarios"
2856
+ }
2857
+ },
2850
2858
  "images": {
2851
2859
  "in": "all"
2852
2860
  },
@@ -4047,7 +4055,9 @@
4047
4055
  },
4048
4056
  "--qr-file": {
4049
4057
  "in": ["tg", "wa"],
4050
- "reason": "ruled tg-only with the app registration it belongs to"
4058
+ "planned": {
4059
+ "max": "max-cli#736, on the cli-core release that exports loginQr"
4060
+ }
4051
4061
  },
4052
4062
  "--sms": {
4053
4063
  "in": ["tg"],
@@ -4066,7 +4076,9 @@
4066
4076
  "--method": "all",
4067
4077
  "--qr-file": {
4068
4078
  "in": ["tg", "wa"],
4069
- "reason": "Telegram can expose its temporary QR login token as a PNG"
4079
+ "planned": {
4080
+ "max": "max-cli#736, on the cli-core release that exports loginQr"
4081
+ }
4070
4082
  }
4071
4083
  }
4072
4084
  },