@wirecat/cli-core 0.527.0 → 0.528.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
@@ -7,6 +7,18 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.528.0 — 01.11.2026
11
+
12
+ ### Added
13
+
14
+ - **Every profile keeps its messenger library's warnings, and `doctor` counts the last day's.** A message the
15
+ library could not decrypt used to leave no trace unless the CLI's own log variable was set. `providerLogPathFor(app,
16
+ profile, env)` names `<state>/provider-log/<profile>.jsonl`; `openProviderLog(path)` makes it private (mode 600)
17
+ and keeps it under 1 MB with one older file, so an engine in another process can append to the path it is given;
18
+ `writeProviderWarning(path, kind, message)` appends from Node. A line is `{"time","level","kind"?,"message"}`, the
19
+ shape zerolog writes; `kind` is the adapter's tag. The adapter masks phone numbers, ids of people and text before
20
+ writing. `doctor` answers `provider_log: { path, last_day: { total, kinds } }` and checks the file is private.
21
+
10
22
  ## 0.527.0 — 01.11.2026
11
23
 
12
24
  ### Added
@@ -23,6 +23,7 @@ export { NO_RULES, NO_TASKS, NOT_ALLOWED, ONLY_TASK, type Replied, type Replier,
23
23
  export { repliesStatePathFor } from "../replies/state.js";
24
24
  export { type CacheEvent, type DiagnosticEvent, type EventSink, providerErrorKey, type RequestEvent, renderEvent, type WarningEvent, } from "../runs/events.js";
25
25
  export { CORE_LOG_EVENTS } from "../runs/log-events.js";
26
+ export { openProviderLog, PROVIDER_LOG_BYTES, type ProviderWarnings, providerLogPathFor, providerWarnings, writeProviderWarning, } from "../runs/provider-log.js";
26
27
  export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
27
28
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
29
  export { runContextOf, secretInput } from "../runs/run-context.js";
package/dist/cli/index.js CHANGED
@@ -19,6 +19,7 @@ export { NO_RULES, NO_TASKS, NOT_ALLOWED, ONLY_TASK, replyTo } from "../replies/
19
19
  export { repliesStatePathFor } from "../replies/state.js";
20
20
  export { providerErrorKey, renderEvent, } from "../runs/events.js";
21
21
  export { CORE_LOG_EVENTS } from "../runs/log-events.js";
22
+ export { openProviderLog, PROVIDER_LOG_BYTES, providerLogPathFor, providerWarnings, writeProviderWarning, } from "../runs/provider-log.js";
22
23
  export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
23
24
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
24
25
  export { runContextOf, secretInput } from "../runs/run-context.js";
@@ -5,6 +5,7 @@ import { annotate } from "@wirecat/cli-base/commands";
5
5
  import { Command } from "commander";
6
6
  import { engineStates } from "../../attachments/extract.js";
7
7
  import { providerErrorKey } from "../../runs/events.js";
8
+ import { providerLogPathFor, providerWarnings } from "../../runs/provider-log.js";
8
9
  import { listRuns, runsDirFor, runtime } from "../../runs/run.js";
9
10
  import { FloodMemory, floodPathFor } from "../../sends/flood.js";
10
11
  import { SendJournal, sendsPathFor } from "../../sends/journal.js";
@@ -20,6 +21,7 @@ import { recalledAccount } from "./accounts.js";
20
21
  import { messengerContext } from "./context.js";
21
22
  import { privateFiles, withSqliteSidecars } from "./private-files.js";
22
23
  import { buildReport, reportFileName } from "./report.js";
24
+ import { DAY_MS } from "./serve-members.js";
23
25
  import { storeSummary } from "./store-maintenance-command.js";
24
26
  /**
25
27
  * The state this installation is in, read from disk and **never from the messenger unless
@@ -121,6 +123,7 @@ const rememberedOf = (report) => report.account?.remembered ?? undefined;
121
123
  * What `doctor` answers, as data. A configuration that will not load is a field, not an exception:
122
124
  * then there is no renderer from the settings, and the caller renders with the defaults.
123
125
  */
126
+ const providerLogState = ({ path, total, kinds }) => ({ path, last_day: { total, kinds } });
124
127
  const diagnose = async (command, messenger) => {
125
128
  const { app, provider } = messenger;
126
129
  const env = environmentOf(command).env ?? process.env;
@@ -183,8 +186,9 @@ const diagnose = async (command, messenger) => {
183
186
  const store = storePath(env);
184
187
  const sends = sendsPathFor(app, profile, env);
185
188
  const flood = floodPathFor(app, profile, env);
189
+ const providerLog = providerLogPathFor(app, profile, env);
186
190
  const files = privateFiles({
187
- files: [...own.files, ...withSqliteSidecars(store), sends, flood],
191
+ files: [...own.files, ...withSqliteSidecars(store), sends, flood, providerLog],
188
192
  // A folder the owner chose with MESSAGING_STORE may rightly be shared, like the home folder itself.
189
193
  dirs: [
190
194
  ...own.dirs,
@@ -192,6 +196,7 @@ const diagnose = async (command, messenger) => {
192
196
  dirname(sends),
193
197
  dirname(flood),
194
198
  runsDirFor(app, env),
199
+ dirname(providerLog),
195
200
  ],
196
201
  });
197
202
  return {
@@ -209,6 +214,7 @@ const diagnose = async (command, messenger) => {
209
214
  readers: engineStates(),
210
215
  sends: sendsState(new SendJournal(sendsPathFor(app, profile, env))),
211
216
  flood: floodState(new FloodMemory(flood)),
217
+ provider_log: providerLogState(providerWarnings(providerLog, new Date(Date.now() - DAY_MS))),
212
218
  runs: { directory: runsDirFor(app, env), kept: listRuns(runsDirFor(app, env)).length },
213
219
  // The messenger's own block keeps the keys it gives: some of them are file paths.
214
220
  files: filesReport(files),
@@ -0,0 +1,22 @@
1
+ import type { AppIdentity } from "../ports/app.js";
2
+ /**
3
+ * The messenger library's warnings for one profile, always kept: a message it failed to decrypt is lost without a
4
+ * trace otherwise. One JSON object per line, `{"time","level","kind"?,"message"}` — the shape zerolog writes, so an
5
+ * engine in another process appends to the file directly. **No phone number, id of a person or message text**: the
6
+ * adapter or its engine masks before writing. `kind` is the adapter's tag (`decrypt`), counted by `doctor`.
7
+ */
8
+ export declare const providerLogPathFor: (app: AppIdentity, profile: string, env?: NodeJS.ProcessEnv) => string;
9
+ export declare const PROVIDER_LOG_BYTES: number;
10
+ /**
11
+ * The file, private and under its cap, ready for an engine to append to. The cap is kept here rather than by the
12
+ * writer, because an engine in another language appends without knowing it: one older file is kept.
13
+ */
14
+ export declare const openProviderLog: (path: string, maxBytes?: number) => string;
15
+ export declare const writeProviderWarning: (path: string, kind: string, message: string, now?: Date) => void;
16
+ export interface ProviderWarnings {
17
+ path: string;
18
+ total: number;
19
+ kinds: Record<string, number>;
20
+ }
21
+ /** Warnings since `since`, by kind; a line without one counts as `other`, a line that is not JSON is skipped. */
22
+ export declare const providerWarnings: (path: string, since: Date) => ProviderWarnings;
@@ -0,0 +1,55 @@
1
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { resolvePaths } from "@wirecat/cli-base";
4
+ /**
5
+ * The messenger library's warnings for one profile, always kept: a message it failed to decrypt is lost without a
6
+ * trace otherwise. One JSON object per line, `{"time","level","kind"?,"message"}` — the shape zerolog writes, so an
7
+ * engine in another process appends to the file directly. **No phone number, id of a person or message text**: the
8
+ * adapter or its engine masks before writing. `kind` is the adapter's tag (`decrypt`), counted by `doctor`.
9
+ */
10
+ export const providerLogPathFor = (app, profile, env = process.env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "provider-log", `${profile}.jsonl`);
11
+ export const PROVIDER_LOG_BYTES = 1024 * 1024;
12
+ const previousOf = (path) => path.replace(/\.jsonl$/, ".1.jsonl");
13
+ /**
14
+ * The file, private and under its cap, ready for an engine to append to. The cap is kept here rather than by the
15
+ * writer, because an engine in another language appends without knowing it: one older file is kept.
16
+ */
17
+ export const openProviderLog = (path, maxBytes = PROVIDER_LOG_BYTES) => {
18
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
19
+ if (existsSync(path) && statSync(path).size >= maxBytes)
20
+ renameSync(path, previousOf(path));
21
+ closeSync(openSync(path, "a", 0o600));
22
+ chmodSync(path, 0o600);
23
+ return path;
24
+ };
25
+ export const writeProviderWarning = (path, kind, message, now = new Date()) => {
26
+ openProviderLog(path);
27
+ appendFileSync(path, `${JSON.stringify({ time: now.toISOString(), level: "warn", kind, message })}\n`);
28
+ };
29
+ /** Warnings since `since`, by kind; a line without one counts as `other`, a line that is not JSON is skipped. */
30
+ export const providerWarnings = (path, since) => {
31
+ const kinds = {};
32
+ let total = 0;
33
+ for (const file of [previousOf(path), path]) {
34
+ if (!existsSync(file))
35
+ continue;
36
+ for (const line of readFileSync(file, "utf8").split("\n")) {
37
+ if (!line)
38
+ continue;
39
+ let entry;
40
+ try {
41
+ entry = JSON.parse(line);
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ const at = typeof entry.time === "string" ? Date.parse(entry.time) : Number.NaN;
47
+ if (!(at >= since.getTime()))
48
+ continue;
49
+ const kind = typeof entry.kind === "string" && entry.kind ? entry.kind : "other";
50
+ kinds[kind] = (kinds[kind] ?? 0) + 1;
51
+ total++;
52
+ }
53
+ }
54
+ return { path, total, kinds };
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.527.0",
3
+ "version": "0.528.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",