@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 +12 -0
- package/dist/cli/schedule/schedule-command.d.ts +0 -1
- package/dist/cli/schedule/schedule-command.js +6 -1
- package/dist/mail/email-input.d.ts +2 -0
- package/dist/mail/email-input.js +6 -1
- package/dist/mail/retry.js +2 -1
- package/dist/mail/sync.js +5 -1
- package/dist/mail/testing/fake.d.ts +2 -0
- package/dist/mail/testing/fake.js +1 -1
- package/dist/store/sqlite/email-locations.d.ts +1 -0
- package/dist/store/sqlite/email-locations.js +4 -0
- package/dist/store/sqlite/emails.d.ts +2 -0
- package/dist/store/sqlite/emails.js +3 -6
- package/package.json +1 -1
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. "
|
|
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">;
|
package/dist/mail/email-input.js
CHANGED
|
@@ -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:
|
|
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,
|
package/dist/mail/retry.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 = {
|
|
@@ -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
|
|
558
|
-
|
|
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.
|
|
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",
|