@wirecat/cli-core 0.354.0 → 0.355.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,21 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.355.0 — 19.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **A failed scheduled run no longer keeps a chat title or a name in `last.json`** (privacy). Its `reason` was the
15
+ last line the run printed, which is the whole error object, message included: "no chat matches …" and the list of
16
+ chats or people an ambiguous name matches reached the file and the "scheduled … failing" notice. `last.json` now
17
+ keeps the error's `code` as the new `errorCode` and a `reason` of the code and its reason id, such as
18
+ `validation_error (input.invalid)`; a last line that is not an error object, such as an agent harness's, is kept
19
+ only as `exit <n>`. The full message stays on stderr and in the run's log, whose path `schedule list` now prints
20
+ after a failing schedule's reason. `last.json` is written atomically with mode 0600 even over an older file.
21
+ `reasonOf(stderr, exitCode?)` follows and always answers a string, and `recordRun` takes the optional `errorCode`.
22
+ A background `store` sync job's file likewise keeps only the error's code and names `store jobs show <id>`, which
23
+ prints the path of the job's log.
24
+
10
25
  ## 0.354.0 — 19.10.2026
11
26
 
12
27
  ### Fixed
@@ -25,15 +25,26 @@ export interface LastRun {
25
25
  output: string;
26
26
  /** Shared by every command the run started (`TRACEPARENT`), so `runs list` names them. */
27
27
  traceId?: string;
28
- /** Why a failed run failed, in one line: the last thing it said, not its whole output. */
28
+ /** Why a failed run failed, in one line: the error's code and reason id, never its message. */
29
29
  reason?: string;
30
+ /** The `code` of the error the run printed, as run records keep it. */
31
+ errorCode?: string;
30
32
  /** The first failure of the failures in a row, so "failing since" survives every retry. */
31
33
  failingSince?: string;
32
34
  /** Failed runs in a row; absent once a run succeeds. */
33
35
  failures?: number;
34
36
  }
35
- /** The last line a failed run printed, which is where a CLI says why it stopped. */
36
- export declare const reasonOf: (output: string) => string | undefined;
37
+ /**
38
+ * Why a failed run failed, from the error object the CLI printed last: its code and reason id. Never the
39
+ * message, which can name a chat or a person; the run's log keeps that. Any other line is another program's
40
+ * text and is not kept either.
41
+ */
42
+ export declare const failureOf: (stderr: string, exitCode: number) => {
43
+ reason: string;
44
+ errorCode?: string;
45
+ };
46
+ /** The one-line reason `failureOf` keeps, for a timer that reads a run's stderr itself. */
47
+ export declare const reasonOf: (stderr: string, exitCode?: number) => string;
37
48
  /**
38
49
  * Writes one run's result to `path`, carrying the failure streak over from the run before. Any CLI's own
39
50
  * timer can call this, so `failingNotice` reads every tool's background runs the same way.
@@ -44,6 +55,7 @@ export declare const recordRun: (path: string, run: {
44
55
  output: string;
45
56
  traceId?: string;
46
57
  reason?: string;
58
+ errorCode?: string;
47
59
  }) => LastRun;
48
60
  /**
49
61
  * One stderr line per failing background job, or nothing: `tg: scheduled digest failing since … (3 runs):
@@ -1,20 +1,30 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { CliError, writeSecurely } from "@wirecat/cli-base";
4
+ import { CliError, errorCodes, writeSecurely } from "@wirecat/cli-base";
5
5
  import { readOnlyLockOf } from "../ports/app.js";
6
6
  import { recordLogFor } from "../runs/record-log.js";
7
7
  import { childTrace } from "../runs/scope.js";
8
- const REASON_CHARS = 200;
9
- /** The last line a failed run printed, which is where a CLI says why it stopped. */
10
- export const reasonOf = (output) => {
11
- const line = output
12
- .split("\n")
13
- .map((text) => text.trim())
14
- .filter(Boolean)
15
- .at(-1);
16
- return line === undefined ? undefined : line.length > REASON_CHARS ? `${line.slice(0, REASON_CHARS - 1)}…` : line;
8
+ const REASON_ID = /^[a-z0-9][a-z0-9.-]{0,79}$/;
9
+ /**
10
+ * Why a failed run failed, from the error object the CLI printed last: its code and reason id. Never the
11
+ * message, which can name a chat or a person; the run's log keeps that. Any other line is another program's
12
+ * text and is not kept either.
13
+ */
14
+ export const failureOf = (stderr, exitCode) => {
15
+ for (const line of stderr.split("\n").reverse()) {
16
+ try {
17
+ const { code, reason } = JSON.parse(line).error ?? {};
18
+ if (errorCodes.includes(code) && typeof reason === "string" && REASON_ID.test(reason)) {
19
+ return { reason: `${code} (${reason})`, errorCode: code };
20
+ }
21
+ }
22
+ catch { }
23
+ }
24
+ return { reason: `exit ${exitCode}` };
17
25
  };
26
+ /** The one-line reason `failureOf` keeps, for a timer that reads a run's stderr itself. */
27
+ export const reasonOf = (stderr, exitCode = 1) => failureOf(stderr, exitCode).reason;
18
28
  const readRun = (path) => {
19
29
  try {
20
30
  return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : undefined;
@@ -29,17 +39,17 @@ const readRun = (path) => {
29
39
  */
30
40
  export const recordRun = (path, run) => {
31
41
  const before = readRun(path);
32
- const { reason, ...rest } = run;
42
+ const { reason, errorCode, ...rest } = run;
33
43
  const last = run.exitCode === 0
34
44
  ? rest
35
45
  : {
36
46
  ...rest,
37
47
  ...(reason === undefined ? {} : { reason }),
48
+ ...(errorCode === undefined ? {} : { errorCode }),
38
49
  failingSince: before?.exitCode ? (before.failingSince ?? before.at) : run.at,
39
50
  failures: before?.exitCode ? (before.failures ?? 1) + 1 : 1,
40
51
  };
41
- mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 });
42
- writeFileSync(path, `${JSON.stringify(last, null, 2)}\n`, { mode: 0o600 });
52
+ writeSecurely(path, `${JSON.stringify(last, null, 2)}\n`, 0o600);
43
53
  return last;
44
54
  };
45
55
  const shortTime = (iso) => {
@@ -371,13 +381,12 @@ export const runSchedule = async (app, system, env, file, { now = () => new Date
371
381
  exitCode,
372
382
  durationMs: ended.getTime() - began,
373
383
  });
374
- const said = exitCode === 0 ? undefined : reasonOf(readFileSync(errors, "utf8"));
375
384
  const last = recordRun(join(folder, "last.json"), {
376
385
  at: ended.toISOString(),
377
386
  exitCode,
378
387
  output,
379
388
  traceId: trace.traceId,
380
- ...(said === undefined ? {} : { reason: said }),
389
+ ...(exitCode === 0 ? {} : failureOf(readFileSync(errors, "utf8"), exitCode)),
381
390
  });
382
391
  if (agent && exitCode === 0)
383
392
  copyFileSync(output, join(folder, "latest.md"));
@@ -175,7 +175,8 @@ export const syncCommand = (messenger) => {
175
175
  catch (error) {
176
176
  if (jobId) {
177
177
  const code = isCliFailure(error) ? error.code : "generic_failure";
178
- const message = error instanceof Error ? error.message : String(error);
178
+ // The job file outlives the run; the message can name a chat, so only the job's log keeps it.
179
+ const message = `${code} — \`store jobs show ${jobId}\` names the log with the whole error`;
179
180
  finishJob(messenger.app, jobs, jobId, { finishedAt: new Date().toISOString(), error: { code, message } });
180
181
  }
181
182
  throw error;
@@ -185,7 +185,7 @@ export const scheduleCommand = (messenger) => {
185
185
  ? `last ${last.at.slice(0, 16).replace("T", " ")} exit ${last.exitCode}` +
186
186
  (last.failures
187
187
  ? ` — failing since ${(last.failingSince ?? last.at).slice(0, 16).replace("T", " ")}, ` +
188
- `${last.failures} in a row${last.reason ? `: ${last.reason}` : ""}`
188
+ `${last.failures} in a row${last.reason ? `: ${last.reason}` : ""} — ${last.output}`
189
189
  : "")
190
190
  : "not run yet",
191
191
  ].join(" "))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.354.0",
3
+ "version": "0.355.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",