@wirecat/cli-core 0.344.0 → 0.346.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 (41) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/cli/bot/command.js +3 -1
  3. package/dist/cli/command-contract.d.ts +13 -0
  4. package/dist/cli/command-contract.js +1 -1
  5. package/dist/cli/context.d.ts +2 -0
  6. package/dist/cli/guide.d.ts +52 -0
  7. package/dist/cli/guide.js +307 -0
  8. package/dist/cli/help/command-help.d.ts +30 -0
  9. package/dist/cli/help/command-help.js +218 -0
  10. package/dist/cli/help/commands-text.d.ts +2 -0
  11. package/dist/cli/help/commands-text.js +1 -1
  12. package/dist/cli/help/first-screen.d.ts +4 -3
  13. package/dist/cli/help/first-screen.js +6 -3
  14. package/dist/cli/help/sections.js +1 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/messenger/chat-answers.js +1 -1
  18. package/dist/cli/messenger/conversations-command.js +4 -1
  19. package/dist/cli/messenger/inbox.js +1 -1
  20. package/dist/cli/messenger/observed.js +15 -9
  21. package/dist/cli/messenger/private-people-command.js +10 -1
  22. package/dist/cli/messenger/review.js +1 -1
  23. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  24. package/dist/cli/messenger/store-maintenance-command.js +8 -5
  25. package/dist/cli/profile.js +1 -1
  26. package/dist/cli/program.js +2 -2
  27. package/dist/domain/models.d.ts +6 -1
  28. package/dist/kit/contract.d.ts +2 -2
  29. package/dist/kit/contract.js +36 -0
  30. package/dist/kit/fake.d.ts +3 -2
  31. package/dist/kit/fake.js +12 -0
  32. package/dist/meetings/mcp/index.d.ts +15 -15
  33. package/dist/services/index.d.ts +3 -0
  34. package/dist/services/index.js +1 -0
  35. package/dist/services/project-gathering-request.d.ts +120 -0
  36. package/dist/services/project-gathering-request.js +217 -0
  37. package/dist/services/project-gathering.d.ts +89 -0
  38. package/dist/services/project-gathering.js +202 -0
  39. package/dist/store/sqlite/chats.js +25 -4
  40. package/package.json +5 -4
  41. package/parity.json +2 -2
@@ -0,0 +1,202 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { createContextEvidenceReader } from "./context-evidence.js";
3
+ import { excerpt } from "./excerpt.js";
4
+ import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
5
+ const terminal = new Set(["done", "unavailable", "failed"]);
6
+ export const gatherProjectEvidence = async (ports, request) => {
7
+ if (!ports?.evidence || typeof ports.evidence.read !== "function")
8
+ throw new CliError("configuration_error", "stored context reads are unavailable in this store");
9
+ const now = ports.now ?? Date.now;
10
+ const started = now();
11
+ const plan = projectGatheringRequest(request, started);
12
+ const previous = plan.checkpoint === undefined ? null : decodeCheckpoint(plan.checkpoint, plan);
13
+ const window = previous ?? plan.window;
14
+ const units = projectUnits(plan, window);
15
+ if (previous && (previous.units.length !== units.length || previous.fetched.length !== plan.sources.length))
16
+ throw new CliError("validation_error", "invalid project gathering checkpoint", { param: "checkpoint" });
17
+ const progress = previous?.units.map((u) => ({ ...u })) ??
18
+ units.map(() => ({ state: "pending", pages: 0, cursor: null, code: null }));
19
+ const fetched = previous?.fetched.map((f) => ({ ...f })) ??
20
+ plan.sources.map(() => ({ state: "not-requested", history: null, code: null }));
21
+ const seen = new Set(previous?.seen ?? []);
22
+ const signal = plan.signal;
23
+ if (plan.fetch === "before-read" && !ports.fetcher)
24
+ throw new CliError("configuration_error", "this host cannot fetch source history; use fetch none", {
25
+ param: "fetch",
26
+ });
27
+ if (plan.fetch === "before-read" && ports.fetcher)
28
+ for (const [index, source] of plan.sources.entries()) {
29
+ if (fetched[index]?.state !== "not-requested")
30
+ continue;
31
+ signal?.throwIfAborted();
32
+ try {
33
+ const answer = await ports.fetcher.fetch({ source, after: window.after, before: window.before, signal });
34
+ if (!["fetched", "unsupported"].includes(answer?.status) ||
35
+ !["complete", "partial", "unknown"].includes(answer?.history))
36
+ throw new CliError("invalid_response", "source fetch returned an unknown status");
37
+ fetched[index] = { state: answer.status, history: answer.history, code: null };
38
+ }
39
+ catch (error) {
40
+ signal?.throwIfAborted();
41
+ if (!(error instanceof CliError))
42
+ throw error;
43
+ fetched[index] = { state: "failed", history: null, code: error.code };
44
+ }
45
+ }
46
+ const usage = { reads: 0, readBytes: 0, items: 0 };
47
+ const items = [];
48
+ const byRef = new Map();
49
+ const matchedAgain = new Map();
50
+ let stopped = null;
51
+ const open = (u) => !terminal.has(u.state) && u.state !== "read-limited";
52
+ const take = (item, lane, kind, source) => {
53
+ const known = byRef.get(item.ref);
54
+ if (known) {
55
+ if (!known.lanes.includes(lane.id))
56
+ known.lanes.push(lane.id);
57
+ if (!lane.kinds.every((k) => k === "person"))
58
+ known.context = "project";
59
+ return;
60
+ }
61
+ const key = seenKey(item.ref);
62
+ if (seen.has(key)) {
63
+ matchedAgain.set(item.ref, (matchedAgain.get(item.ref) ?? new Set()).add(lane.id));
64
+ return;
65
+ }
66
+ seen.add(key);
67
+ const short = excerpt(item.text, lane.query, 300, { literal: true });
68
+ const entry = {
69
+ ...item,
70
+ kind,
71
+ source,
72
+ text: short,
73
+ textTruncated: short !== item.text.replace(/\s+/g, " ").trim(),
74
+ lanes: [lane.id],
75
+ context: lane.kinds.every((k) => k === "person") ? "person-only" : "project",
76
+ };
77
+ byRef.set(item.ref, entry);
78
+ items.push(entry);
79
+ usage.items++;
80
+ };
81
+ for (let round = 0; !stopped; round++) {
82
+ const due = progress.flatMap((u, index) => (open(u) && u.pages <= round ? [index] : []));
83
+ if (!due.length)
84
+ break;
85
+ for (const index of due) {
86
+ const unit = units[index], state = progress[index];
87
+ if (!unit || !state)
88
+ continue;
89
+ signal?.throwIfAborted();
90
+ stopped =
91
+ usage.reads >= plan.budgets.maxReads
92
+ ? "reads"
93
+ : usage.readBytes >= plan.budgets.maxReadBytes
94
+ ? "read-bytes"
95
+ : now() - started >= plan.budgets.maxElapsedMs
96
+ ? "elapsed"
97
+ : usage.items >= plan.budgets.maxItems
98
+ ? "items"
99
+ : null;
100
+ if (stopped)
101
+ break;
102
+ const allowance = Math.min(plan.budgets.maxPageBytes, plan.budgets.maxReadBytes - usage.readBytes);
103
+ const reader = createContextEvidenceReader(ports.evidence, [unit.source], {
104
+ maxReads: 1,
105
+ maxTotalReadBytes: allowance,
106
+ });
107
+ usage.reads++;
108
+ try {
109
+ const page = await reader.read({
110
+ source: unit.source,
111
+ query: unit.lane.query,
112
+ after: unit.chunk.after,
113
+ before: unit.chunk.before,
114
+ limit: 20,
115
+ maxReadBytes: allowance,
116
+ ...(state.cursor ? { cursor: state.cursor } : {}),
117
+ signal,
118
+ });
119
+ usage.readBytes += page.readBytes;
120
+ state.pages++;
121
+ state.code = page.coverage.index === "ready" ? null : `index-${page.coverage.index}`;
122
+ for (const item of page.items)
123
+ take(item, unit.lane, unit.source.kind, unit.sourceIndex);
124
+ if (!page.coverage.available)
125
+ state.state = "unavailable";
126
+ else if (page.next) {
127
+ state.state = "partial";
128
+ state.cursor = page.next;
129
+ }
130
+ else {
131
+ state.state = "done";
132
+ state.cursor = null;
133
+ }
134
+ }
135
+ catch (error) {
136
+ signal?.throwIfAborted();
137
+ usage.readBytes += reader.usage().chargedBytes;
138
+ if (!(error instanceof CliError))
139
+ throw error;
140
+ if (error.code === "validation_error" && error.details?.reason === "context.read-budget")
141
+ state.state = "read-limited";
142
+ else
143
+ state.state = error.code === "not_found" ? "unavailable" : "failed";
144
+ state.code = error.code;
145
+ }
146
+ }
147
+ }
148
+ const totals = {
149
+ reads: (previous?.totals.reads ?? 0) + usage.reads,
150
+ readBytes: (previous?.totals.readBytes ?? 0) + usage.readBytes,
151
+ items: (previous?.totals.items ?? 0) + usage.items,
152
+ invocations: (previous?.totals.invocations ?? 0) + 1,
153
+ };
154
+ const resumable = progress.some((u) => !terminal.has(u.state));
155
+ const checkpoint = {
156
+ version: 1,
157
+ runKey: plan.runKey,
158
+ fingerprint: plan.fingerprint,
159
+ after: window.after,
160
+ before: window.before,
161
+ asOf: window.asOf,
162
+ fetched,
163
+ units: progress.map((u) => (u.state === "read-limited" ? { ...u, state: "partial" } : u)),
164
+ seen: [...seen],
165
+ totals,
166
+ };
167
+ return {
168
+ schemaVersion: 1,
169
+ runKey: plan.runKey,
170
+ fingerprint: plan.fingerprint,
171
+ resumed: previous !== null,
172
+ project: plan.project,
173
+ window: {
174
+ after: window.after,
175
+ before: window.before,
176
+ asOf: window.asOf,
177
+ lookbackDays: plan.lookbackDays,
178
+ basis: plan.lookbackDays === null ? "explicit" : "lookback",
179
+ timeBasis: "per-item-time-kind",
180
+ },
181
+ scope: { origin: plan.scope, sources: plan.sources },
182
+ lanes: plan.lanes,
183
+ fetch: fetched.map((f, index) => ({ source: index, ...f })),
184
+ units: units.map((unit, index) => ({
185
+ lane: unit.lane.id,
186
+ source: unit.sourceIndex,
187
+ after: unit.chunk.after,
188
+ before: unit.chunk.before,
189
+ state: progress[index]?.state ?? "pending",
190
+ pages: progress[index]?.pages ?? 0,
191
+ issue: progress[index]?.code ?? null,
192
+ })),
193
+ items,
194
+ matchedAgain: [...matchedAgain].map(([ref, lanes]) => ({ ref, lanes: [...lanes] })),
195
+ usage: { ...usage, elapsedMs: now() - started },
196
+ totals,
197
+ stopped,
198
+ complete: !resumable,
199
+ completeness: "unknown",
200
+ checkpoint: resumable ? encodeCheckpoint(checkpoint) : null,
201
+ };
202
+ };
@@ -4,7 +4,7 @@ import { identityPk } from "./identities.js";
4
4
  import { bound } from "./query-filter.js";
5
5
  import { chatEventPeople, chatEvents, chatMembers, chats, chunks, fetchLeases, identities, involvements, memberCounts, memberObservationMembers, memberObservations, memberStays, messageRevisions, messages, messageTranscripts, pollVotes, syncRanges, } from "./schema.js";
6
6
  import { ensureTag } from "./tags.js";
7
- import { json, parsed, present, toIso, toMs } from "./values.js";
7
+ import { parsed, present, toIso, toMs } from "./values.js";
8
8
  /** The chat's own id, when the store key is a chat of this account. */
9
9
  export const chatOf = ({ orm }, accountKey, chatKey) => orm
10
10
  .select({ id: chats.externalId })
@@ -31,7 +31,12 @@ export const chatPkFor = (context, accountKey, chatId) => findChatPk(context, ac
31
31
  .get()?.pk);
32
32
  export const upsertChat = (context, accountKey, chat) => {
33
33
  const { orm, now } = context;
34
- const parent = chat.parentChatId === undefined ? null : chatPkFor(context, accountKey, chat.parentChatId);
34
+ const parent = chat.parentChatId == null ? null : chatPkFor(context, accountKey, chat.parentChatId);
35
+ const stored = orm
36
+ .select({ metadata: chats.metadata })
37
+ .from(chats)
38
+ .where(and(eq(chats.accountId, accountKey), eq(chats.externalId, chat.id)))
39
+ .get();
35
40
  orm
36
41
  .insert(chats)
37
42
  .values({
@@ -44,7 +49,7 @@ export const upsertChat = (context, accountKey, chat) => {
44
49
  unreadCount: chat.unreadCount,
45
50
  lastMessageAt: toMs(chat.lastMessageAt),
46
51
  participantsCount: chat.participantsCount,
47
- metadata: json(chat.providerMetadata),
52
+ metadata: mergedMetadata(stored?.metadata ?? null, chat.providerMetadata),
48
53
  membershipState: chat.membershipState ?? null,
49
54
  createdAt: now(),
50
55
  updatedAt: now(),
@@ -53,7 +58,7 @@ export const upsertChat = (context, accountKey, chat) => {
53
58
  target: [chats.accountId, chats.externalId],
54
59
  set: {
55
60
  kind: sql `excluded.kind`,
56
- parentChatId: sql `coalesce(excluded.parent_chat_id, ${chats.parentChatId})`,
61
+ ...(chat.parentChatId === undefined ? {} : { parentChatId: sql `excluded.parent_chat_id` }),
57
62
  scope: sql `coalesce(excluded.scope, ${chats.scope})`,
58
63
  title: sql `excluded.title`,
59
64
  unreadCount: sql `excluded.unread_count`,
@@ -68,6 +73,22 @@ export const upsertChat = (context, accountKey, chat) => {
68
73
  })
69
74
  .run();
70
75
  };
76
+ const mergedMetadata = (stored, incoming) => {
77
+ if (incoming === undefined)
78
+ return stored;
79
+ const merged = Object.entries({ ...metadataObject(stored), ...incoming }).filter(([, value]) => value !== null);
80
+ return merged.length === 0 ? null : JSON.stringify(Object.fromEntries(merged));
81
+ };
82
+ // Stored metadata that is not an object has no keys to keep.
83
+ const metadataObject = (stored) => {
84
+ try {
85
+ const value = stored === null ? null : JSON.parse(stored);
86
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
87
+ }
88
+ catch {
89
+ return {};
90
+ }
91
+ };
71
92
  /** Replaces who is in the chat, whole. */
72
93
  export const writeMembers = (context, key, accountKey, chatId, memberIds) => {
73
94
  const chatKey = chatPkFor(context, accountKey, chatId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.344.0",
3
+ "version": "0.346.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",
@@ -158,7 +158,8 @@
158
158
  "standards:check": "node config/wirecat/check.mjs",
159
159
  "docs:lint": "markdownlint-cli2",
160
160
  "docs:spell": "cspell lint --config cspell.json --no-progress README.md",
161
- "ci:quick": "pnpm typecheck && vitest run src/cli/command-lint.test.ts"
161
+ "ci:quick": "pnpm typecheck && vitest run src/cli/command-lint.test.ts",
162
+ "help:examples": "HELP_EXAMPLES_REPORT=1 vitest run scripts/help-examples.test.ts --silent=false"
162
163
  },
163
164
  "dependencies": {
164
165
  "@anthropic-ai/sdk": "0.131.0",
@@ -201,7 +202,7 @@
201
202
  "@types/node": "^22.20.4",
202
203
  "@types/pngjs": "6.0.5",
203
204
  "@vitest/coverage-v8": "^5.0.2",
204
- "@wirecat/cli-base": "0.45.0",
205
+ "@wirecat/cli-base": "0.46.0",
205
206
  "commander": "15.0.0",
206
207
  "cspell": "10.3.6",
207
208
  "drizzle-kit": "1.0.0-rc.4",
@@ -221,7 +222,7 @@
221
222
  },
222
223
  "peerDependencies": {
223
224
  "@napi-rs/canvas": ">=0.1.69 <2",
224
- "@wirecat/cli-base": ">=0.45.0 <0.46.0",
225
+ "@wirecat/cli-base": ">=0.45.0 <0.47.0",
225
226
  "commander": ">=15"
226
227
  },
227
228
  "bin": {
package/parity.json CHANGED
@@ -133,7 +133,7 @@
133
133
  "meaning": "also read these bots' copies, comma separated — each allowed by read_other_bots"
134
134
  },
135
135
  "--brief": {
136
- "meaning": "found messages and mail as reference, title, time, sender and excerpt only"
136
+ "meaning": "found messages as reference, title, time, sender and excerpt only"
137
137
  },
138
138
  "--by": {
139
139
  "value": "<grouping>",
@@ -394,7 +394,7 @@
394
394
  "meaning": "read nonrecursive files from this directory for one explicit chat"
395
395
  },
396
396
  "--full": {
397
- "meaning": "found messages and mail with their whole text and every provider field"
397
+ "meaning": "found messages with their whole text and every provider field"
398
398
  },
399
399
  "--hidden": {
400
400
  "value": "<on|off>",