@wirecat/cli-core 0.553.0 → 0.554.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.554.0 — 02.11.2026
11
+
12
+ ### Fixed
13
+
14
+ - **`schedule agent` help no longer shows `skill show follow-ups` to a CLI without that skill.** The prompt example is
15
+ the follow-up review for a messenger and a read-only `search messages` summary for a CLI with `messenger: false`
16
+ (mail).
17
+ - **Mail sync stores `sent_at`.** It is the Date header, or the server's internal date when the header is missing or
18
+ unreadable; `received_at` is unchanged. Each sync also gives up to 5,000 emails stored without `sent_at` the internal
19
+ date (`MailStore.backfillSentAt`), so the `emails_by_time` index becomes useful and an account fills in about a day.
20
+ No schema change.
21
+
10
22
  ## 0.553.0 — 02.11.2026
11
23
 
12
24
  ### Added
@@ -1,7 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { type AppIdentity } from "../../ports/app.js";
3
3
  import { type AnswerOutput } from "../envelope.js";
4
- /** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
5
4
  export interface ScheduleHost {
6
5
  app: AppIdentity;
7
6
  context(command: Command, resolving?: {
@@ -68,6 +68,11 @@ const withWhen = (command) => command
68
68
  .option("--every <duration>", "repeat: 30m, 2h, 1d")
69
69
  .option("--at <HH:MM>", "every day at this local time")
70
70
  .option("--at-time <time>", "one run: 30m, 2d from now, or an ISO time; the schedule removes itself after it");
71
+ /** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
72
+ /** `skill show follow-ups` exists only for messenger CLIs; any other CLI gets a read-only summary it can run. */
73
+ const promptExample = (app) => app.messenger === false
74
+ ? `summarize what came in during the last day: ${app.command} personal search messages 'date>1d' --newest --limit 20`
75
+ : `run the daily follow-up review: ${app.command} skill show follow-ups`;
71
76
  /**
72
77
  * Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
73
78
  * (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
@@ -142,7 +147,7 @@ export const scheduleCommandFor = ({ app, context: contextOf }) => {
142
147
  .description(`start an agent (claude or codex) with a prompt on a timer; it runs with ${app.envPrefix}_READ_ONLY=1, so it reads and drafts and cannot send — its answer is kept and you are notified`)
143
148
  .argument("<name>", "lowercase letters, digits and dashes")
144
149
  .addArgument(new Argument("<harness>", "the agent to start").choices(HARNESSES))
145
- .argument("<prompt>", `what to do, e.g. "run the daily follow-up review: ${app.command} skill show follow-ups"`)
150
+ .argument("<prompt>", `what to do, e.g. "${promptExample(app)}"`)
146
151
  .addOption(new Option("--max-cost <usd>", "claude only: stop a run that costs more than this, in US dollars").argParser(Number))).action(async function (name, harness, prompt) {
147
152
  const { context, system, environment, save } = prepare(this);
148
153
  const options = this.opts();
@@ -1,5 +1,7 @@
1
1
  import type { EmailInput } from "../store/index.js";
2
2
  import type { ParsedMail } from "./parse.js";
3
3
  import type { Listed } from "./source.js";
4
+ /** The Date header, or the server's internal date when the header is missing or unreadable. */
5
+ export declare const sentAtOf: (date: string | null, receivedAt: string) => number;
4
6
  /** What one read email stores of itself, whichever run read it; where it is filed is the caller's. */
5
7
  export declare const parsedEmail: (mail: ParsedMail, item: Listed, header: Record<string, string | boolean>, address: string) => Omit<EmailInput, "mailboxes">;
@@ -1,4 +1,9 @@
1
1
  import { addressOf } from "./attachments.js";
2
+ /** The Date header, or the server's internal date when the header is missing or unreadable. */
3
+ export const sentAtOf = (date, receivedAt) => {
4
+ const sent = date === null ? Number.NaN : Date.parse(date);
5
+ return Number.isNaN(sent) ? Date.parse(receivedAt) : sent;
6
+ };
2
7
  /** What one read email stores of itself, whichever run read it; where it is filed is the caller's. */
3
8
  export const parsedEmail = (mail, item, header, address) => ({
4
9
  externalId: item.messageId,
@@ -7,7 +12,7 @@ export const parsedEmail = (mail, item, header, address) => ({
7
12
  to: mail.to.map(addressOf),
8
13
  cc: mail.cc.map(addressOf),
9
14
  bcc: mail.bcc.map(addressOf),
10
- sentAt: null,
15
+ sentAt: sentAtOf(mail.date, item.receivedAt),
11
16
  receivedAt: Date.parse(item.receivedAt),
12
17
  bodyText: mail.text || null,
13
18
  size: item.size ?? null,
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { mailAccountKey } from "./account-key.js";
3
3
  import { addressOf, FETCH_CHUNK, readAttachments } from "./attachments.js";
4
+ import { sentAtOf } from "./email-input.js";
4
5
  import { mailFolders } from "./folders.js";
5
6
  import { mailHeadersOf } from "./headers.js";
6
7
  import { parseMail } from "./parse.js";
@@ -74,7 +75,7 @@ export const retryUnread = async ({ store, source, account, since, max, maxBodyC
74
75
  to: mail.to.map(addressOf),
75
76
  cc: mail.cc.map(addressOf),
76
77
  bcc: mail.bcc.map(addressOf),
77
- sentAt: null,
78
+ sentAt: sentAtOf(mail.date, item.receivedAt),
78
79
  receivedAt: Date.parse(item.receivedAt),
79
80
  bodyText: mail.text || null,
80
81
  size: item.size ?? stored.size,
package/dist/mail/sync.js CHANGED
@@ -24,6 +24,8 @@ const DAY = 86_400_000;
24
24
  const EDGE = 2 * DAY;
25
25
  /** How often a long run saves where it is, so a kill costs at most this much listing again. */
26
26
  const SAVE_EVERY_MS = 30_000;
27
+ /** About 55 runs clear a 276,000-email account; each is a short write that does not hold the sync up. */
28
+ const SENT_AT_BACKFILL_PER_RUN = 5000;
27
29
  const SLICE_SIZE = 50;
28
30
  const SLICE_PAUSE_MS = 100;
29
31
  /** About a minute of listing; the 30-day window of the timer stays below it. */
@@ -682,7 +684,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
682
684
  // Subject and text stay out of the columns search reads, so recorded mail is never a search hit.
683
685
  subject: null,
684
686
  from: headers?.from === undefined ? null : { address: headers.from, name: headers.fromName ?? null },
685
- sentAt: null,
687
+ sentAt: Date.parse(item.receivedAt),
686
688
  receivedAt: Date.parse(item.receivedAt),
687
689
  bodyText: null,
688
690
  size: item.size ?? null,
@@ -816,6 +818,8 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
816
818
  await store.mail.setMailboxes(accountId, [bulkBox], bulk, startedAt);
817
819
  if (!snapshot)
818
820
  await store.sync.setSyncState(key, "mail_folder_scope", scope);
821
+ if (!snapshot)
822
+ await store.mail.backfillSentAt(accountId, SENT_AT_BACKFILL_PER_RUN);
819
823
  await stopIfAborted();
820
824
  const indexed = await index?.(store, key);
821
825
  const coverage = {
@@ -12,6 +12,8 @@ export interface FakeMail {
12
12
  from: FakePerson;
13
13
  to: FakePerson[];
14
14
  cc?: FakePerson[];
15
+ /** The Date header, when it differs from the server's internal date `receivedAt`. */
16
+ date?: string;
15
17
  text?: string;
16
18
  html?: string;
17
19
  listId?: string;
@@ -31,7 +31,7 @@ export const rawOf = (m) => {
31
31
  ...(m.listId ? [`List-Id: News <${m.listId}>`, `List-Unsubscribe: <mailto:leave@${m.listId}>`] : []),
32
32
  `Subject: ${m.subject}`,
33
33
  `Message-ID: <${m.messageId}@example.test>`,
34
- `Date: ${new Date(m.receivedAt).toUTCString()}`,
34
+ `Date: ${m.date ?? new Date(m.receivedAt).toUTCString()}`,
35
35
  "MIME-Version: 1.0",
36
36
  ...(m.extraHeaders ?? []),
37
37
  ];
@@ -59,3 +59,4 @@ export declare const partialMail: (database: CacheDatabase) => PartialMailStore;
59
59
  export declare const storedWhole: (database: CacheDatabase, emailId: number) => boolean;
60
60
  export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
61
61
  export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
62
+ export declare const backfillSentAt: (database: CacheDatabase, accountId: number, limit: number) => number;
@@ -107,3 +107,7 @@ export const forgetLocations = (database, accountId, folder, uidvalidity, uids)
107
107
  for (const emailId of new Set(emails))
108
108
  leave.run(emailId, Number(box.id), emailId, Number(box.id));
109
109
  };
110
+ export const backfillSentAt = (database, accountId, limit) => Number(database
111
+ .prepare(`UPDATE emails SET sent_at = received_at WHERE id IN (
112
+ SELECT id FROM emails WHERE account_id = ? AND sent_at IS NULL AND received_at IS NOT NULL LIMIT ?)`)
113
+ .run(accountId, limit).changes);
@@ -238,6 +238,8 @@ export interface MailStore extends PartialMailStore {
238
238
  read?: boolean;
239
239
  flagged?: boolean;
240
240
  }>, now: number): Promise<number>;
241
+ /** Gives up to `limit` emails stored without `sent_at` their internal date; answers how many. */
242
+ backfillSentAt(accountId: number, limit: number): Promise<number>;
241
243
  /** The live emails whose location in `folder` has this UIDVALIDITY, oldest UID first. */
242
244
  folderEmails(accountId: number, folder: string, uidvalidity: string): Promise<FolderEmail[]>;
243
245
  /**
@@ -2,7 +2,7 @@ import { formatLocator } from "../../domain/locator.js";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { fold, normalize } from "../normalize.js";
4
4
  import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
5
- import { folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
5
+ import { backfillSentAt, folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
6
6
  import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
7
7
  import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
8
8
  import { folded } from "./folded.js";
@@ -554,11 +554,8 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
554
554
  return count;
555
555
  });
556
556
  },
557
- async setFlags(accountId, flags, now) {
558
- return write(database, () => {
559
- return setEmailFlags(database, accountId, flags, now);
560
- });
561
- },
557
+ setFlags: async (accountId, flags, now) => write(database, () => setEmailFlags(database, accountId, flags, now)),
558
+ backfillSentAt: async (accountId, limit) => write(database, () => backfillSentAt(database, accountId, limit)),
562
559
  folderEmails: async (accountId, folder, uidvalidity) => folderEmails(database, accountId, folder, uidvalidity),
563
560
  setLocations: async (accountId, folder, uidvalidity, found) => write(database, () => setLocations(database, accountId, folder, uidvalidity, found)),
564
561
  forgetLocations: async (accountId, folder, uidvalidity, uids) => write(database, () => forgetLocations(database, accountId, folder, uidvalidity, uids)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.553.0",
3
+ "version": "0.554.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",