@wirecat/cli-core 0.347.0 → 0.349.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 (76) hide show
  1. package/CHANGELOG.md +82 -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 +24 -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/context/evidence.d.ts +10 -0
  31. package/dist/domain/models.d.ts +11 -1
  32. package/dist/kit/contract.d.ts +2 -2
  33. package/dist/kit/contract.js +25 -0
  34. package/dist/kit/fake.d.ts +1 -0
  35. package/dist/kit/fake.js +15 -16
  36. package/dist/mcp/meeting-preparation.d.ts +1 -0
  37. package/dist/mcp/meeting-project-candidates.d.ts +1 -0
  38. package/dist/mcp/meeting-series-inventory.d.ts +1 -0
  39. package/dist/mcp/tool.js +2 -0
  40. package/dist/mcp/tools/messages-photo.js +2 -1
  41. package/dist/meetings/mcp/passage.d.ts +1 -0
  42. package/dist/meetings/mcp/project-evidence.d.ts +1 -0
  43. package/dist/meetings/mcp/search.d.ts +1 -0
  44. package/dist/meetings/reads.d.ts +1 -0
  45. package/dist/ports/messenger.d.ts +10 -2
  46. package/dist/runs/recording.js +1 -0
  47. package/dist/runs/scope.d.ts +7 -0
  48. package/dist/runs/scope.js +5 -0
  49. package/dist/services/archive.js +2 -1
  50. package/dist/services/chats.js +3 -2
  51. package/dist/services/context-evidence.d.ts +1 -1
  52. package/dist/services/conversations.js +2 -1
  53. package/dist/services/deps.js +2 -1
  54. package/dist/services/embeddings-maintenance.js +3 -2
  55. package/dist/services/index.d.ts +6 -4
  56. package/dist/services/index.js +1 -0
  57. package/dist/services/project-gathering-around.d.ts +37 -0
  58. package/dist/services/project-gathering-around.js +57 -0
  59. package/dist/services/project-gathering-request.d.ts +13 -1
  60. package/dist/services/project-gathering-request.js +23 -5
  61. package/dist/services/project-gathering.d.ts +15 -1
  62. package/dist/services/project-gathering.js +32 -1
  63. package/dist/services/search-all-meetings.d.ts +3 -4
  64. package/dist/services/search-all.d.ts +8 -1
  65. package/dist/services/search-all.js +1 -1
  66. package/dist/speech/transcribe.js +2 -1
  67. package/dist/store/chats.d.ts +2 -2
  68. package/dist/store/sqlite/chats.d.ts +2 -2
  69. package/dist/store/sqlite/chats.js +7 -7
  70. package/dist/store/sqlite/context-evidence.d.ts +4 -1
  71. package/dist/store/sqlite/context-evidence.js +12 -0
  72. package/dist/store/store.d.ts +2 -1
  73. package/dist/store/store.js +5 -1
  74. package/dist/store/types.d.ts +2 -2
  75. package/package.json +3 -3
  76. package/parity.json +2 -12
@@ -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.
@@ -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,5 +5,5 @@ export interface ScopedContextEvidenceReader extends ContextEvidenceReader {
5
5
  usage(): ContextReadUsage;
6
6
  }
7
7
  export declare const createContextEvidenceReader: (port: ContextEvidenceReader, allowed: readonly ContextSource[], limits?: ContextReadLimits) => ScopedContextEvidenceReader;
8
- export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextSource, } from "../context/evidence.js";
8
+ export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextSource, } from "../context/evidence.js";
9
9
  export type { ContextReadLimits, ContextReadUsage } from "../context/read-budget.js";
@@ -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;
@@ -1,10 +1,12 @@
1
1
  import { type BotCheckService } from "../botcheck/service.js";
2
- export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextReadLimits, ContextReadUsage, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
2
+ export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextReadLimits, ContextReadUsage, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
3
3
  export { createContextEvidenceReader } from "./context-evidence.js";
4
4
  export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
5
- export type { ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectSourceFetcher, } from "./project-gathering.js";
5
+ export type { ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
6
6
  export { gatherProjectEvidence } from "./project-gathering.js";
7
- export type { ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, } from "./project-gathering-request.js";
7
+ export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
8
+ export { storedMessageNeighbours } from "./project-gathering-around.js";
9
+ export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectUnitState, } from "./project-gathering-request.js";
8
10
  import { type AccountService } from "./account.js";
9
11
  import { type AdminService } from "./admin.js";
10
12
  import { type AdminStatisticsService } from "./admin-statistics.js";
@@ -156,7 +158,7 @@ export { privatePeopleService } from "./private-people.js";
156
158
  export { approveAsTask, type ProposalTaskStore, proposalSource } from "./proposal-tasks.js";
157
159
  export type { RetentionQuery, RetentionService } from "./retention.js";
158
160
  export { retentionService } from "./retention.js";
159
- export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
161
+ export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
160
162
  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
163
  export { CATCH_UP_BOUNDS, type CatchUpOptions, type CatchUpResult, catchUpSearch, validateCatchUp, } from "./search-catchup.js";
162
164
  export { accountsOfKind, listContextOf, type SearchKind } from "./search-kind.js";
@@ -2,6 +2,7 @@ import { botCheckService } from "../botcheck/service.js";
2
2
  export { createContextEvidenceReader } from "./context-evidence.js";
3
3
  export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
4
4
  export { gatherProjectEvidence } from "./project-gathering.js";
5
+ export { storedMessageNeighbours } from "./project-gathering-around.js";
5
6
  import { accountService } from "./account.js";
6
7
  import { adminService } from "./admin.js";
7
8
  import { adminStatisticsService } from "./admin-statistics.js";
@@ -0,0 +1,37 @@
1
+ import type { MessagesStore } from "../store/messages.js";
2
+ export interface ProjectNeighbour {
3
+ ref: string;
4
+ at: number;
5
+ sender: string | null;
6
+ text: string;
7
+ found: boolean;
8
+ }
9
+ /** The stored messages next to a hit in its chat, oldest first, without the hit itself. */
10
+ export interface ProjectNeighbourReader {
11
+ around(ref: string, window: {
12
+ before: number;
13
+ after: number;
14
+ }): Promise<{
15
+ ref: string;
16
+ at: number;
17
+ sender: string | null;
18
+ text: string;
19
+ }[]>;
20
+ }
21
+ export declare const storedMessageNeighbours: (messages: Pick<MessagesStore, "around">) => ProjectNeighbourReader;
22
+ /**
23
+ * Neighbours are context for reading a hit, not matches: they never join `items`, and their text counts against
24
+ * the run's read bytes.
25
+ */
26
+ export declare const attachNeighbours: (items: {
27
+ ref: string;
28
+ kind: string;
29
+ around?: ProjectNeighbour[] | null;
30
+ aroundIssue?: string | null;
31
+ }[], reader: ProjectNeighbourReader | undefined, window: {
32
+ before: number;
33
+ after: number;
34
+ }, budget: {
35
+ remaining: () => number;
36
+ charge: (bytes: number) => void;
37
+ }, known: (ref: string) => boolean, signal?: AbortSignal) => Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { formatLocator, parseLocator } from "../domain/locator.js";
3
+ import { excerpt } from "./excerpt.js";
4
+ export const storedMessageNeighbours = (messages) => ({
5
+ async around(ref, window) {
6
+ const at = parseLocator(ref);
7
+ const rows = await messages.around({ provider: at.provider, account: at.account }, at.chat, at.message, window);
8
+ return rows
9
+ .filter((row) => !row.anchor)
10
+ .map((row) => ({
11
+ ref: formatLocator({ ...at, message: row.id }),
12
+ at: Date.parse(row.timestamp),
13
+ sender: row.senderName,
14
+ text: row.text ?? "",
15
+ }));
16
+ },
17
+ });
18
+ /**
19
+ * Neighbours are context for reading a hit, not matches: they never join `items`, and their text counts against
20
+ * the run's read bytes.
21
+ */
22
+ export const attachNeighbours = async (items, reader, window, budget, known, signal) => {
23
+ if (!reader || window.before + window.after === 0)
24
+ return;
25
+ for (const item of items) {
26
+ if (item.kind !== "message")
27
+ continue;
28
+ if (budget.remaining() <= 0) {
29
+ item.around = null;
30
+ item.aroundIssue = "read-bytes";
31
+ continue;
32
+ }
33
+ signal?.throwIfAborted();
34
+ try {
35
+ const rows = await reader.around(item.ref, window);
36
+ const around = [];
37
+ for (const row of rows.slice(0, window.before + window.after)) {
38
+ const text = excerpt(row.text, "", 200);
39
+ const bytes = Buffer.byteLength(text);
40
+ if (bytes > budget.remaining()) {
41
+ item.aroundIssue = "read-bytes";
42
+ break;
43
+ }
44
+ budget.charge(bytes);
45
+ around.push({ ...row, text, found: known(row.ref) });
46
+ }
47
+ item.around = around;
48
+ }
49
+ catch (error) {
50
+ signal?.throwIfAborted();
51
+ if (!(error instanceof CliError))
52
+ throw error;
53
+ item.around = null;
54
+ item.aroundIssue = error.code;
55
+ }
56
+ }
57
+ };
@@ -2,6 +2,7 @@ import { type ContextSource } from "../context/evidence.js";
2
2
  export interface ProjectGatheringRequest {
3
3
  project: {
4
4
  name: string;
5
+ key?: string;
5
6
  background?: string;
6
7
  aliases?: string[];
7
8
  };
@@ -17,6 +18,11 @@ export interface ProjectGatheringRequest {
17
18
  before?: number;
18
19
  asOf?: number;
19
20
  fetch?: "none" | "before-read";
21
+ index?: "update" | "as-is";
22
+ around?: {
23
+ before: number;
24
+ after: number;
25
+ };
20
26
  budgets?: ProjectGatheringBudgets;
21
27
  runKey?: string;
22
28
  checkpoint?: string;
@@ -29,7 +35,7 @@ export interface ProjectGatheringBudgets {
29
35
  maxElapsedMs?: number;
30
36
  maxItems?: number;
31
37
  }
32
- export type ProjectLaneKind = "project" | "alias" | "keyword" | "person";
38
+ export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person";
33
39
  export interface ProjectLane {
34
40
  id: string;
35
41
  kinds: ProjectLaneKind[];
@@ -73,6 +79,7 @@ export declare const CONTEXT_CHUNK_DAYS = 90;
73
79
  export declare const projectGatheringRequest: (input: ProjectGatheringRequest, now: number) => {
74
80
  project: {
75
81
  name: string;
82
+ key: string | null;
76
83
  background: string | null;
77
84
  aliases: string[];
78
85
  };
@@ -91,6 +98,11 @@ export declare const projectGatheringRequest: (input: ProjectGatheringRequest, n
91
98
  asOf: number;
92
99
  };
93
100
  fetch: "before-read" | "none";
101
+ index: "as-is" | "update";
102
+ around: {
103
+ before: number;
104
+ after: number;
105
+ };
94
106
  budgets: Required<ProjectGatheringBudgets>;
95
107
  fingerprint: string;
96
108
  runKey: string;
@@ -55,6 +55,8 @@ export const projectGatheringRequest = (input, now) => {
55
55
  "before",
56
56
  "asOf",
57
57
  "fetch",
58
+ "index",
59
+ "around",
58
60
  "budgets",
59
61
  "runKey",
60
62
  "checkpoint",
@@ -62,9 +64,10 @@ export const projectGatheringRequest = (input, now) => {
62
64
  ], "");
63
65
  if (!input.project || typeof input.project !== "object")
64
66
  fail("project needs a name", "project");
65
- known(input.project, ["name", "background", "aliases"], "project.");
67
+ known(input.project, ["name", "key", "background", "aliases"], "project.");
66
68
  const project = {
67
69
  name: text(input.project.name, "project.name", 512),
70
+ key: input.project.key === undefined ? null : text(input.project.key, "project.key", 64),
68
71
  background: input.project.background === undefined ? null : text(input.project.background, "project.background", 8192),
69
72
  aliases: list(input.project.aliases, "project.aliases", LIMITS.lanes).map((a, i) => text(a, `project.aliases.${i}`, 512)),
70
73
  };
@@ -104,6 +107,8 @@ export const projectGatheringRequest = (input, now) => {
104
107
  found.people.push(person);
105
108
  };
106
109
  lane("project", laneQuery(project.name, "project.name"));
110
+ if (project.key)
111
+ lane("key", laneQuery(project.key, "project.key"));
107
112
  for (const [i, a] of project.aliases.entries())
108
113
  lane("alias", laneQuery(a, `project.aliases.${i}`));
109
114
  for (const k of keywords)
@@ -113,19 +118,30 @@ export const projectGatheringRequest = (input, now) => {
113
118
  if (lanes.length > LIMITS.lanes)
114
119
  fail(`at most ${LIMITS.lanes} distinct search lanes`, "keywords");
115
120
  const fetch = input.fetch ?? "none";
121
+ const index = input.index ?? "update";
122
+ if (index !== "update" && index !== "as-is")
123
+ fail("index must be update or as-is", "index");
124
+ if (input.around !== undefined && (!input.around || typeof input.around !== "object"))
125
+ fail("around needs before and after", "around");
126
+ if (input.around)
127
+ known(input.around, ["before", "after"], "around.");
128
+ const around = {
129
+ before: whole(input.around?.before ?? 2, "around.before", 0, 5),
130
+ after: whole(input.around?.after ?? 2, "around.after", 0, 5),
131
+ };
116
132
  if (fetch !== "none" && fetch !== "before-read")
117
133
  fail("fetch must be none or before-read", "fetch");
118
134
  const explicit = input.after !== undefined || input.before !== undefined;
119
135
  if (explicit && input.lookbackDays !== undefined)
120
136
  fail("give lookbackDays or after/before, not both", "lookbackDays");
121
- if (explicit && (input.after === undefined || input.before === undefined))
122
- fail("an explicit window needs both after and before", input.after === undefined ? "after" : "before");
137
+ if (explicit && input.after === undefined)
138
+ fail("an explicit window needs after; before defaults to asOf", "after");
123
139
  const asOf = input.asOf === undefined ? null : whole(input.asOf, "asOf", 1, MAX_TIME);
124
140
  const lookbackDays = explicit ? null : whole(input.lookbackDays ?? 60, "lookbackDays", 1, LIMITS.lookbackDays);
125
141
  const window = explicit
126
142
  ? (() => {
127
143
  const after = whole(input.after, "after", 0, MAX_TIME);
128
- const before = whole(input.before, "before", after + 1, MAX_TIME);
144
+ const before = whole(input.before ?? asOf ?? now, "before", after + 1, MAX_TIME);
129
145
  if (before - after > LIMITS.lookbackDays * DAY)
130
146
  fail(`window must not exceed ${LIMITS.lookbackDays} days`, "after");
131
147
  return { after, before, asOf: asOf ?? before };
@@ -150,7 +166,7 @@ export const projectGatheringRequest = (input, now) => {
150
166
  sources.map(contextSourceKey),
151
167
  input.scope,
152
168
  lookbackDays,
153
- explicit ? [window.after, window.before] : null,
169
+ explicit ? [window.after, input.before ?? null] : null,
154
170
  asOf,
155
171
  fetch,
156
172
  ]))
@@ -168,6 +184,8 @@ export const projectGatheringRequest = (input, now) => {
168
184
  lookbackDays,
169
185
  window,
170
186
  fetch,
187
+ index,
188
+ around,
171
189
  budgets,
172
190
  fingerprint,
173
191
  runKey,
@@ -1,4 +1,5 @@
1
- import type { ContextEvidenceItem, ContextEvidenceReader, ContextSource } from "../context/evidence.js";
1
+ import type { ContextEvidenceItem, ContextEvidenceReader, ContextIndex, ContextSource } from "../context/evidence.js";
2
+ import { type ProjectNeighbour, type ProjectNeighbourReader } from "./project-gathering-around.js";
2
3
  import { type ProjectFetchState, type ProjectGatheringRequest, type ProjectLane } from "./project-gathering-request.js";
3
4
  /** A connector's history fetch into the store; the store reads that follow find what it saved. */
4
5
  export interface ProjectSourceFetcher {
@@ -15,8 +16,16 @@ export interface ProjectSourceFetcher {
15
16
  export interface ProjectGatheringPorts {
16
17
  evidence: ContextEvidenceReader;
17
18
  fetcher?: ProjectSourceFetcher;
19
+ index?: ContextIndex;
20
+ neighbours?: ProjectNeighbourReader;
18
21
  now?: () => number;
19
22
  }
23
+ export interface ProjectIndexCatchUp {
24
+ state: "updated" | "as-is" | "unavailable" | "failed";
25
+ mail: number;
26
+ documents: number;
27
+ code: string | null;
28
+ }
20
29
  export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
21
30
  kind: ContextSource["kind"];
22
31
  source: number;
@@ -24,6 +33,8 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
24
33
  textTruncated: boolean;
25
34
  lanes: string[];
26
35
  context: "project" | "person-only";
36
+ around?: ProjectNeighbour[] | null;
37
+ aroundIssue?: string | null;
27
38
  }
28
39
  export type ProjectGatheringStop = "reads" | "read-bytes" | "elapsed" | "items" | null;
29
40
  export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, request: ProjectGatheringRequest) => Promise<{
@@ -33,6 +44,7 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
33
44
  resumed: boolean;
34
45
  project: {
35
46
  name: string;
47
+ key: string | null;
36
48
  background: string | null;
37
49
  aliases: string[];
38
50
  };
@@ -55,6 +67,7 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
55
67
  code: string | null;
56
68
  source: number;
57
69
  }[];
70
+ index: ProjectIndexCatchUp;
58
71
  units: {
59
72
  lane: string;
60
73
  source: number;
@@ -72,6 +85,7 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
72
85
  usage: {
73
86
  reads: number;
74
87
  readBytes: number;
88
+ aroundBytes: number;
75
89
  items: number;
76
90
  elapsedMs: number;
77
91
  };
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { createContextEvidenceReader } from "./context-evidence.js";
3
3
  import { excerpt } from "./excerpt.js";
4
+ import { attachNeighbours } from "./project-gathering-around.js";
4
5
  import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
5
6
  const terminal = new Set(["done", "unavailable", "failed"]);
6
7
  export const gatherProjectEvidence = async (ports, request) => {
@@ -12,6 +13,9 @@ export const gatherProjectEvidence = async (ports, request) => {
12
13
  const previous = plan.checkpoint === undefined ? null : decodeCheckpoint(plan.checkpoint, plan);
13
14
  const window = previous ?? plan.window;
14
15
  const units = projectUnits(plan, window);
16
+ // A default that cannot finish one round would leave whole searches or sources unread; an explicit one is kept.
17
+ if (request.budgets?.maxReads === undefined)
18
+ plan.budgets.maxReads = Math.min(512, Math.max(48, units.length));
15
19
  if (previous && (previous.units.length !== units.length || previous.fetched.length !== plan.sources.length))
16
20
  throw new CliError("validation_error", "invalid project gathering checkpoint", { param: "checkpoint" });
17
21
  const progress = previous?.units.map((u) => ({ ...u })) ??
@@ -43,7 +47,27 @@ export const gatherProjectEvidence = async (ports, request) => {
43
47
  fetched[index] = { state: "failed", history: null, code: error.code };
44
48
  }
45
49
  }
46
- const usage = { reads: 0, readBytes: 0, items: 0 };
50
+ const indexed = { state: "as-is", mail: 0, documents: 0, code: null };
51
+ const kinds = [...new Set(plan.sources.map((source) => source.kind))].filter((kind) => kind !== "message");
52
+ if (plan.index === "update" && kinds.length) {
53
+ signal?.throwIfAborted();
54
+ if (!ports.index || typeof ports.index.catchUp !== "function")
55
+ indexed.state = "unavailable";
56
+ else
57
+ try {
58
+ const done = await ports.index.catchUp({ kinds, signal });
59
+ if (!Number.isSafeInteger(done?.mail) || !Number.isSafeInteger(done?.documents))
60
+ throw new CliError("invalid_response", "index catch-up returned invalid counts");
61
+ Object.assign(indexed, { state: "updated", mail: done.mail, documents: done.documents });
62
+ }
63
+ catch (error) {
64
+ signal?.throwIfAborted();
65
+ if (!(error instanceof CliError))
66
+ throw error;
67
+ Object.assign(indexed, { state: "failed", code: error.code });
68
+ }
69
+ }
70
+ const usage = { reads: 0, readBytes: 0, aroundBytes: 0, items: 0 };
47
71
  const items = [];
48
72
  const byRef = new Map();
49
73
  const matchedAgain = new Map();
@@ -145,6 +169,12 @@ export const gatherProjectEvidence = async (ports, request) => {
145
169
  }
146
170
  }
147
171
  }
172
+ await attachNeighbours(items, ports.neighbours, plan.around, {
173
+ remaining: () => plan.budgets.maxReadBytes - usage.readBytes - usage.aroundBytes,
174
+ charge: (bytes) => {
175
+ usage.aroundBytes += bytes;
176
+ },
177
+ }, (ref) => byRef.has(ref) || seen.has(seenKey(ref)), signal);
148
178
  const totals = {
149
179
  reads: (previous?.totals.reads ?? 0) + usage.reads,
150
180
  readBytes: (previous?.totals.readBytes ?? 0) + usage.readBytes,
@@ -181,6 +211,7 @@ export const gatherProjectEvidence = async (ports, request) => {
181
211
  scope: { origin: plan.scope, sources: plan.sources },
182
212
  lanes: plan.lanes,
183
213
  fetch: fetched.map((f, index) => ({ source: index, ...f })),
214
+ index: indexed,
184
215
  units: units.map((unit, index) => ({
185
216
  lane: unit.lane.id,
186
217
  source: unit.sourceIndex,
@@ -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[];