@wirecat/cli-core 0.409.0 → 0.410.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## 0.410.0 — 24.10.2026
9
+
10
+ ### Added
11
+
12
+ - `buildDoctorReport`, `doctorReportFileName` and their report types are public through `/cli`.
13
+ Consumers can reuse run selection, identifier labelling and home-path redaction while retaining
14
+ typed diagnostic payloads and optional technical context. Existing reports keep their shape
15
+ when context is omitted. `eventFields: "recorded"` retains allowlisted recorded technical
16
+ fields for consumers that already include them; the default report projection is unchanged.
17
+
18
+ ### Fixed
19
+
20
+ - **Meeting search continuation arguments match the generated MCP schema.** Stored numeric selector IDs are
21
+ serialized as strings in replay arguments, so an agent can pass the returned next step to the shared read tool
22
+ without converting IDs. Validated search requests still use numeric store IDs.
23
+
24
+ - Media archives skip call events alongside service events and keyboards. A message containing
25
+ both a file and a call event now archives the file and verifies its checksum; call-only
26
+ messages make no download request. Unknown attachment kinds still fail rather than being
27
+ silently dropped.
28
+
8
29
  ## 0.409.0 — 24.10.2026
9
30
 
10
31
  ### Added
@@ -112,6 +112,7 @@ export { type ModeProblem, type PrivateFiles, privateFiles, withSqliteSidecars }
112
112
  export { reactionsCommand } from "./messenger/reactions-command.js";
113
113
  export { callsCommand, stickersCommand } from "./messenger/records-command.js";
114
114
  export { repliesCommand } from "./messenger/replies-command.js";
115
+ export { buildReport as buildDoctorReport, type Report as DoctorReport, type ReportContext as DoctorReportContext, reportFileName as doctorReportFileName, } from "./messenger/report.js";
115
116
  export { reviewCommand } from "./messenger/review.js";
116
117
  export { scheduleCommand } from "./messenger/schedule-command.js";
117
118
  export { allAnswer, allHitItem, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, } from "./messenger/search-answers.js";
package/dist/cli/index.js CHANGED
@@ -106,6 +106,7 @@ export { privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
106
106
  export { reactionsCommand } from "./messenger/reactions-command.js";
107
107
  export { callsCommand, stickersCommand } from "./messenger/records-command.js";
108
108
  export { repliesCommand } from "./messenger/replies-command.js";
109
+ export { buildReport as buildDoctorReport, reportFileName as doctorReportFileName, } from "./messenger/report.js";
109
110
  export { reviewCommand } from "./messenger/review.js";
110
111
  export { scheduleCommand } from "./messenger/schedule-command.js";
111
112
  export { allAnswer, allHitItem, messageHitItem, noteHitItem, } from "./messenger/search-answers.js";
@@ -1,9 +1,9 @@
1
1
  import { type RunMetadata } from "../../runs/run.js";
2
2
  import type { SendEntry } from "../../sends/journal.js";
3
- export interface Report {
3
+ export interface Report<Doctor extends object = Record<string, unknown>> {
4
4
  createdAt: string;
5
5
  /** What `doctor --json` answers — versions, paths, counts and the messenger's own checks. */
6
- doctor: Record<string, unknown>;
6
+ doctor: Doctor;
7
7
  /** The run the report is about: the one asked for, or the newest that failed. */
8
8
  run: {
9
9
  metadata: RunMetadata;
@@ -12,13 +12,25 @@ export interface Report {
12
12
  /** The newest write attempts — outcomes and ids, never text (`src/sends/journal.ts`). */
13
13
  sends: SendEntry[];
14
14
  }
15
+ /** Optional technical context supplied by a consumer; it is redacted with the rest of the report. */
16
+ export interface ReportContext {
17
+ createdAt?: never;
18
+ doctor?: never;
19
+ run?: never;
20
+ sends?: never;
21
+ version?: string;
22
+ runtime?: string;
23
+ platform?: string;
24
+ arch?: string;
25
+ profile?: string | null;
26
+ }
15
27
  /**
16
28
  * **Everything a report holds is already free of content** — the doctor, the run log and the send
17
29
  * journal are built that way. Two things are added: every id becomes a label, and the home
18
- * directory is hidden, since paths name the person's account on this machine. Copied from max-cli.
30
+ * directory is hidden, since paths name the person's account on this machine.
19
31
  */
20
- export declare const buildReport: ({ doctor, runsDir, runId, command, sends, now, home, }: {
21
- doctor: Record<string, unknown>;
32
+ export declare const buildReport: <Doctor extends object, Context extends ReportContext = Record<never, never>>({ doctor, runsDir, runId, command, sends, now, home, context, eventFields, }: {
33
+ doctor: Doctor;
22
34
  runsDir: string;
23
35
  runId?: string;
24
36
  /** What to type to list runs, for the error when `runId` names none. */
@@ -26,7 +38,10 @@ export declare const buildReport: ({ doctor, runsDir, runId, command, sends, now
26
38
  sends: SendEntry[];
27
39
  now?: Date;
28
40
  home: string | undefined;
29
- }) => Report;
41
+ context?: Context;
42
+ /** Retain allowlisted recorded fields, including time and severity, for an existing consumer contract. */
43
+ eventFields?: "report" | "recorded";
44
+ }) => Report<Doctor> & Omit<Context, keyof Report>;
30
45
  /**
31
46
  * Every field of a journal line that names a chat, a message or a write. A send's `operationId` is
32
47
  * its `sendId`, so one left raw would undo the other's label.
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { CliError } from "@wirecat/cli-base";
4
+ import { LOG_ATTRIBUTES } from "../../runs/log-attributes.js";
4
5
  import { findRun, listRuns, readEvents } from "../../runs/run.js";
5
6
  const RECENT_SENDS = 20;
6
7
  /** Only these fields of a run event travel; a field added later is left out until it is named here. */
@@ -18,21 +19,22 @@ const EVENT_FIELDS = [
18
19
  /**
19
20
  * **Everything a report holds is already free of content** — the doctor, the run log and the send
20
21
  * journal are built that way. Two things are added: every id becomes a label, and the home
21
- * directory is hidden, since paths name the person's account on this machine. Copied from max-cli.
22
+ * directory is hidden, since paths name the person's account on this machine.
22
23
  */
23
- export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new Date(), home, }) => {
24
+ export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new Date(), home, context, eventFields = "report", }) => {
24
25
  const chosen = runId === undefined ? newestFailed(runsDir) : findRun(runsDir, runId);
25
26
  if (runId !== undefined && !chosen)
26
27
  throw new CliError("not_found", `no run ${runId} — \`${command} runs list\` names them`);
27
28
  const label = labeller();
28
29
  const account = doctor.account;
29
30
  const report = {
31
+ ...context,
30
32
  createdAt: now.toISOString(),
31
33
  doctor: {
32
34
  ...doctor,
33
35
  ...(account?.remembered ? { account: { ...account, remembered: label(account.remembered) } } : {}),
34
36
  },
35
- run: chosen ? { metadata: chosen.metadata, events: readEvents(chosen.dir).map(eventOf(label)) } : null,
37
+ run: chosen ? { metadata: chosen.metadata, events: readEvents(chosen.dir).map(eventOf(label, eventFields)) } : null,
36
38
  sends: sends.slice(-RECENT_SENDS).map((entry) => labelled(entry, label)),
37
39
  };
38
40
  return JSON.parse(hideHome(JSON.stringify(report), home));
@@ -85,8 +87,21 @@ const labeller = () => {
85
87
  const salt = randomBytes(16);
86
88
  return (id) => `id:${createHash("sha256").update(salt).update(id).digest("hex").slice(0, 12)}`;
87
89
  };
88
- const eventOf = (label) => (event) => {
89
- const kept = Object.fromEntries(Object.entries(event).filter(([key]) => EVENT_FIELDS.includes(key)));
90
+ const RECORDED_FIELDS = new Set([...LOG_ATTRIBUTES, "time", "severity"]);
91
+ const RECORDED_ID_FIELDS = new Set([
92
+ ...SEND_ID_FIELDS.map((key) => key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)),
93
+ "actor_id",
94
+ "target_id",
95
+ ]);
96
+ const eventOf = (label, fields) => (event) => {
97
+ const kept = Object.fromEntries(Object.entries(event)
98
+ .filter(([key]) => (fields === "recorded" ? RECORDED_FIELDS.has(key) : EVENT_FIELDS.includes(key)))
99
+ .map(([key, value]) => [
100
+ key,
101
+ fields === "recorded" && RECORDED_ID_FIELDS.has(key) && value !== undefined && value !== null
102
+ ? label(String(value))
103
+ : value,
104
+ ]));
90
105
  const ids = kept.ids;
91
106
  if (typeof ids !== "object" || ids === null)
92
107
  return kept;
@@ -6,6 +6,7 @@ export const NOT_FILES = new Set([
6
6
  "location",
7
7
  "contact",
8
8
  "control",
9
+ "call",
9
10
  "inline_keyboard",
10
11
  ]);
11
12
  /** The stored kinds `has:<kind>` matches: MAX stores a file as `file`, Telegram as `document`. */
@@ -81,11 +81,11 @@ export const meetingHitSearchRequest = (query, options, signal) => {
81
81
  /** Parsed dates must become ISO values again so the CLI can replay its own continuation. */
82
82
  export const meetingSearchArguments = (query, options) => Object.fromEntries(Object.entries({
83
83
  query,
84
- meeting_id: options.meetingId,
85
- identity_id: options.identityId,
86
- person_id: options.personId,
87
- event_id: options.eventId,
88
- series_id: options.seriesId,
84
+ meeting_id: options.meetingId?.toString(),
85
+ identity_id: options.identityId?.toString(),
86
+ person_id: options.personId?.toString(),
87
+ event_id: options.eventId?.toString(),
88
+ series_id: options.seriesId?.toString(),
89
89
  project: options.project,
90
90
  tag: options.tag,
91
91
  topic: options.topic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.409.0",
3
+ "version": "0.410.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",
@@ -209,7 +209,7 @@
209
209
  "@types/node": "^22.20.4",
210
210
  "@types/pngjs": "6.0.5",
211
211
  "@vitest/coverage-v8": "^5.0.2",
212
- "@wirecat/cli-base": "0.50.2",
212
+ "@wirecat/cli-base": "0.50.4",
213
213
  "commander": "15.0.0",
214
214
  "cspell": "10.3.6",
215
215
  "drizzle-kit": "1.0.0-rc.4",