@wirecat/cli-core 0.586.0 → 0.588.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,44 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.588.0 — 06.11.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **Store version 52 is amended in place: deals and leads in their own tables, a stage history, and no review.**
15
+ 0.585.0, 0.586.0 and 0.587.0 shipped an earlier shape of version 52, but no published CLI depends on them, so no real store
16
+ holds it; a store a test or a local build moved to 52 with them must be made again. Tables `deals` and `leads`
17
+ (refs `deal:<id>`, `lead:<id>`) replace `opportunities`; a lead may name the deal it became (`deal_id`). Pipelines
18
+ have a ref, `pipeline:<id>`, belong to the owner or to a CRM account (`account_id`, `external_id`, so two accounts
19
+ may each have "Sales"), and hold deals or leads (`kind`); stages have an `outcome` (`open`, `won`, `lost`) and an
20
+ `external_id`, and positions are not unique. Deals and leads keep the CRM's `account_id`, `external_id` and a board
21
+ `rank`. Triggers write every stage a deal or a lead enters to `pipeline_stage_changes`. The migration seeds one lead
22
+ pipeline, `Leads`, with the stages `new`, `contacted`, `qualified`, `won` and `lost`.
23
+ - **`store.deals` writes for the owner and his agents alike.** The `deal` proposal kind and the review columns are
24
+ gone; only the author is kept. `add`, `list` take `kind` (`deal` by default; a lead may leave out its stage when one
25
+ lead pipeline is stored). A pipeline is named by `pipeline:<id>` or by a name only one pipeline has; a shared name
26
+ is refused with the candidates (`pipeline.ambiguous`). Moving to a stage whose outcome is won or lost closes the row,
27
+ and `close` moves it to its pipeline's stage of that outcome when there is one. `store.deals.pipeline(ref)` reports
28
+ each stage's open rows, how many rows reached it and the conversion to the next stage, with won, lost and win rate.
29
+
30
+ ## 0.587.0 — 06.11.2026
31
+
32
+ ### Added
33
+
34
+ - **`answerLocally` and `wallClock` in `@wirecat/cli-core/cli`.** `answerLocally` answers as `renderAnswer` does, and a
35
+ person reads each UTC time as `YYYY-MM-DD HH:MM` in the zone in use, with one `times in <zone>` line on stderr; a
36
+ `view` gets the formatter to build its text with. JSON, JSONL, `--compact` and agents keep UTC. `hitText` takes the
37
+ formatter as an optional third argument. mail can drop its own copy (`src/commands/times.ts`).
38
+ - **cli-core accepts `@wirecat/cli-base` 0.80 (peer range `>=0.76.0 <0.81.0`) and builds with 0.80.0**, which
39
+ adds the `--write <id>` flag mail's `messages restore` takes. The range was `<0.80.0`.
40
+
41
+ ### Fixed
42
+
43
+ - **`search messages`, `messages show`, `conversations list|show`, mail `review`, `runs list` and `runs show` show a
44
+ person times in the local zone** (WC-TIME-4), with a last line `times in <zone>`. They printed UTC ISO strings
45
+ such as `2026-10-03T09:00:00.000Z`. `search messages --timezone` decides the zone shown. JSON and storage keep UTC.
46
+ `runs list|show` is shared by every tool on cli-core; the mail screens reach mail and memo at their next bump.
47
+
10
48
  ## 0.586.0 — 06.11.2026
11
49
 
12
50
  ### Added
@@ -1,26 +1,16 @@
1
1
  import { renderPretty } from "@wirecat/cli-base";
2
- const UTC_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u;
2
+ import { UTC_TIME, wallClock } from "../../cli/local-times.js";
3
3
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
4
  /**
5
5
  * A calendar answer as a person reads it at a terminal (WC-TIME-4): each UTC time on the wall clock of `zone`, an
6
6
  * all-day event's times as its local dates, and a last line naming the zone. JSON keeps the UTC times.
7
7
  */
8
8
  export const localTimesView = (value, zone, color) => {
9
- const clock = new Intl.DateTimeFormat("en-CA", {
10
- timeZone: zone,
11
- year: "numeric",
12
- month: "2-digit",
13
- day: "2-digit",
14
- hour: "2-digit",
15
- minute: "2-digit",
16
- hourCycle: "h23",
17
- });
9
+ const clock = wallClock(zone);
18
10
  let shown = false;
19
11
  const local = (text, dateOnly) => {
20
- const part = Object.fromEntries(clock.formatToParts(Date.parse(text)).map(({ type, value }) => [type, value]));
21
12
  shown = true;
22
- const date = `${part.year}-${part.month}-${part.day}`;
23
- return dateOnly ? date : `${date} ${part.hour}:${part.minute}`;
13
+ return dateOnly ? clock.date(text) : clock.time(text);
24
14
  };
25
15
  const walk = (one, allDay = false) => {
26
16
  if (typeof one === "string")
@@ -86,6 +86,7 @@ export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
86
86
  export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
87
87
  export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
88
88
  export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, negatableOf, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
89
+ export { answerLocally, type ShownTime, wallClock } from "./local-times.js";
89
90
  export { MCP_HTTP_DEFAULT_PORT, type McpServerOptions, type McpServerTarget, mcpServerCommand, } from "./mcp-server-command.js";
90
91
  export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
91
92
  export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
package/dist/cli/index.js CHANGED
@@ -77,6 +77,7 @@ export { inSection, SECTIONS } from "./help/sections.js";
77
77
  export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
78
78
  export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
79
79
  export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, negatableOf, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
80
+ export { answerLocally, wallClock } from "./local-times.js";
80
81
  export { MCP_HTTP_DEFAULT_PORT, mcpServerCommand, } from "./mcp-server-command.js";
81
82
  export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
82
83
  export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
@@ -0,0 +1,16 @@
1
+ import type { Command } from "commander";
2
+ import { type AnswerOutput, type Body } from "./envelope.js";
3
+ export declare const UTC_TIME: RegExp;
4
+ /** `YYYY-MM-DD HH:MM` on the wall clock of `zone`, with no offset: the answer names the zone once. */
5
+ export declare const wallClock: (zone: string) => {
6
+ date: (iso: string) => string;
7
+ time: (iso: string) => string;
8
+ };
9
+ /** Turns a UTC time into the shown one; any other text comes back as it was. */
10
+ export type ShownTime = (value: string) => string;
11
+ /**
12
+ * Answers as `renderAnswer` does, and a person reads every UTC time in the zone in use (`zone`, else the computer's),
13
+ * with one line naming it (WC-TIME-4). A `view` builds its text with the `shown` it is given. JSON, JSONL, compact
14
+ * rows and agents keep UTC (WC-TIME-2), and `next` is left alone: its args run.
15
+ */
16
+ export declare const answerLocally: <T>(command: Command, output: AnswerOutput, body: Body<T>, view?: (shown: ShownTime) => string, zone?: string) => void;
@@ -0,0 +1,65 @@
1
+ import { timezoneOf } from "@wirecat/cli-base/query";
2
+ import { environmentOf } from "./context.js";
3
+ import { renderAnswer } from "./envelope.js";
4
+ export const UTC_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u;
5
+ /** `YYYY-MM-DD HH:MM` on the wall clock of `zone`, with no offset: the answer names the zone once. */
6
+ export const wallClock = (zone) => {
7
+ const format = new Intl.DateTimeFormat("en-CA", {
8
+ timeZone: zone,
9
+ year: "numeric",
10
+ month: "2-digit",
11
+ day: "2-digit",
12
+ hour: "2-digit",
13
+ minute: "2-digit",
14
+ hourCycle: "h23",
15
+ });
16
+ const parts = (iso) => Object.fromEntries(format.formatToParts(Date.parse(iso)).map(({ type, value }) => [type, value]));
17
+ const date = (iso) => {
18
+ const part = parts(iso);
19
+ return `${part.year}-${part.month}-${part.day}`;
20
+ };
21
+ return {
22
+ date,
23
+ time: (iso) => {
24
+ const part = parts(iso);
25
+ return `${part.year}-${part.month}-${part.day} ${part.hour}:${part.minute}`;
26
+ },
27
+ };
28
+ };
29
+ const localised = (value, shown) => {
30
+ if (typeof value === "string")
31
+ return shown(value);
32
+ if (Array.isArray(value))
33
+ return value.map((one) => localised(one, shown));
34
+ if (value !== null && typeof value === "object")
35
+ return Object.fromEntries(Object.entries(value).map(([key, one]) => [key, localised(one, shown)]));
36
+ return value;
37
+ };
38
+ /**
39
+ * Answers as `renderAnswer` does, and a person reads every UTC time in the zone in use (`zone`, else the computer's),
40
+ * with one line naming it (WC-TIME-4). A `view` builds its text with the `shown` it is given. JSON, JSONL, compact
41
+ * rows and agents keep UTC (WC-TIME-2), and `next` is left alone: its args run.
42
+ */
43
+ export const answerLocally = (command, output, body, view, zone) => {
44
+ if (output.format !== "pretty" || output.compact || environmentOf(command).agent) {
45
+ renderAnswer(command, output, body, view && (() => view((value) => value)));
46
+ return;
47
+ }
48
+ const inUse = timezoneOf(zone);
49
+ const clock = wallClock(inUse);
50
+ let seen = false;
51
+ const shown = (value) => {
52
+ if (!UTC_TIME.test(value))
53
+ return value;
54
+ seen = true;
55
+ return clock.time(value);
56
+ };
57
+ if (view)
58
+ renderAnswer(command, output, body, () => view(shown));
59
+ else
60
+ renderAnswer(command, output, ("items" in body
61
+ ? { ...body, items: body.items.map((one) => localised(one, shown)) }
62
+ : { ...body, item: localised(body.item, shown) }));
63
+ if (seen)
64
+ output.renderer.note(`times in ${inUse}`);
65
+ };
@@ -6,6 +6,7 @@ import { environmentOf, outputFor } from "../context.js";
6
6
  import { LOCAL_READ, NO_EFFECTS } from "../effects.js";
7
7
  import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
8
8
  import { typedQuery, withQuery } from "../list-query.js";
9
+ import { answerLocally } from "../local-times.js";
9
10
  import { positiveCount } from "../paging.js";
10
11
  import { lineOf } from "../steps.js";
11
12
  import { RUNS_QUERY, runListFilter } from "./query.js";
@@ -33,7 +34,7 @@ export const runsCommand = (app) => {
33
34
  const { renderer } = output;
34
35
  const runs = listRuns(dir(this), { atLeast: page * limit + 1, filter });
35
36
  const shown = runs.slice((page - 1) * limit, page * limit);
36
- renderAnswer(this, output, listOf("runs list", {
37
+ answerLocally(this, output, listOf("runs list", {
37
38
  items: shown.map(snakeKeys),
38
39
  limit,
39
40
  page,
@@ -60,7 +61,7 @@ export const runsCommand = (app) => {
60
61
  const { problems } = this.opts();
61
62
  const shown = problems ? events.filter(notable) : events;
62
63
  // Events are log records as written; only the run's own fields are renamed here.
63
- renderAnswer(this, output, itemOf({
64
+ answerLocally(this, output, itemOf({
64
65
  ...snakeKeys(found.metadata),
65
66
  directory: found.dir,
66
67
  event_count: events.length,
@@ -1,9 +1,14 @@
1
- import type { FinanceReview } from "./finance.js";
2
- /** Whether a deal ended, kept apart from the stage it stands in. */
1
+ /** A pipeline holds deals or leads, never both; a deal and a lead are rows of their own tables. */
2
+ export declare const DEAL_KINDS: readonly ["deal", "lead"];
3
+ /** Whether a deal or a lead ended, kept apart from the stage it stands in. */
3
4
  export declare const DEAL_STATUSES: readonly ["open", "won", "lost"];
5
+ /** What reaching a stage means, as a CRM marks it: still open, or won or lost. */
6
+ export declare const STAGE_OUTCOMES: readonly ["open", "won", "lost"];
4
7
  /** The part a person plays on a deal: the owner's side runs it, the client's side talks or decides. */
5
8
  export declare const DEAL_ROLES: readonly ["owner", "contact", "decision-maker"];
9
+ export type DealKind = (typeof DEAL_KINDS)[number];
6
10
  export type DealStatus = (typeof DEAL_STATUSES)[number];
11
+ export type StageOutcome = DealStatus;
7
12
  export type DealRole = (typeof DEAL_ROLES)[number];
8
13
  export interface DealTitleFields {
9
14
  id: number;
@@ -12,9 +17,6 @@ export interface DealTitleFields {
12
17
  amount: number | null;
13
18
  currency: string | null;
14
19
  status: DealStatus;
15
- review: FinanceReview;
16
20
  }
17
- /** "Site redesign · Proposal, 500 000.00 RUB": a ref summary's title, which `refLabel` prefixes. */
21
+ /** "Site redesign · Proposal, 500 000.00 RUB (won)": a ref summary's title, which `refLabel` prefixes. */
18
22
  export declare const dealLine: (deal: DealTitleFields) => string;
19
- /** "Deal Site redesign · Proposal, 500 000.00 RUB (won)". */
20
- export declare const dealTitle: (deal: DealTitleFields) => string;
@@ -1,15 +1,14 @@
1
1
  import { moneyLabel } from "./money.js";
2
- /** Whether a deal ended, kept apart from the stage it stands in. */
2
+ /** A pipeline holds deals or leads, never both; a deal and a lead are rows of their own tables. */
3
+ export const DEAL_KINDS = ["deal", "lead"];
4
+ /** Whether a deal or a lead ended, kept apart from the stage it stands in. */
3
5
  export const DEAL_STATUSES = ["open", "won", "lost"];
6
+ /** What reaching a stage means, as a CRM marks it: still open, or won or lost. */
7
+ export const STAGE_OUTCOMES = DEAL_STATUSES;
4
8
  /** The part a person plays on a deal: the owner's side runs it, the client's side talks or decides. */
5
9
  export const DEAL_ROLES = ["owner", "contact", "decision-maker"];
6
- /** "Site redesign · Proposal, 500 000.00 RUB": a ref summary's title, which `refLabel` prefixes. */
10
+ /** "Site redesign · Proposal, 500 000.00 RUB (won)": a ref summary's title, which `refLabel` prefixes. */
7
11
  export const dealLine = (deal) => {
8
12
  const money = deal.amount === null || deal.currency === null ? "" : `, ${moneyLabel(deal.amount, deal.currency)}`;
9
- const notes = [deal.status === "open" ? null : deal.status, deal.review === "confirmed" ? null : deal.review]
10
- .filter(Boolean)
11
- .join(", ");
12
- return `${deal.title} · ${deal.stage}${money}${notes ? ` (${notes})` : ""}`;
13
+ return `${deal.title} · ${deal.stage}${money}${deal.status === "open" ? "" : ` (${deal.status})`}`;
13
14
  };
14
- /** "Deal Site redesign · Proposal, 500 000.00 RUB (won)". */
15
- export const dealTitle = (deal) => `Deal ${dealLine(deal)}`;
@@ -53,6 +53,8 @@ const WORDS = {
53
53
  payment: "Payment",
54
54
  order: "Order",
55
55
  deal: "Deal",
56
+ lead: "Lead",
57
+ pipeline: "Pipeline",
56
58
  email: "Email",
57
59
  email_thread: "Email thread",
58
60
  unknown: "Unknown",
@@ -33,7 +33,7 @@ export type Reference = {
33
33
  * Things named by their store id (a task also by its key or the task package's id). `entity` is the name
34
34
  * before organizations and projects: it still parses, so an old file's reference resolves as not found.
35
35
  */
36
- export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "event", "invoice", "payment", "order", "deal", "entity"];
36
+ export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "event", "invoice", "payment", "order", "deal", "lead", "pipeline", "entity"];
37
37
  export type SimpleReference = (typeof SIMPLE_REFERENCES)[number];
38
38
  export type RefKindName = Exclude<Reference["type"], "entity">;
39
39
  export interface RefKindSpec<R extends Reference = Reference> {
@@ -20,6 +20,8 @@ export const SIMPLE_REFERENCES = [
20
20
  "payment",
21
21
  "order",
22
22
  "deal",
23
+ "lead",
24
+ "pipeline",
23
25
  "entity",
24
26
  ];
25
27
  const malformed = (text, shape) => new CliError("validation_error", `"${singleLine(text)}" is not a reference — expected ${shape}`, {
@@ -203,14 +203,14 @@ const OTHERS = [
203
203
  validation("money.amount", "the amount is not a decimal string in the currency's scale"),
204
204
  validation("money.currency", "the currency is not one the store keeps money in"),
205
205
  validation("money.quantity", "the quantity is not a decimal above 0"),
206
- validation("deal.closed", "a won, lost, rejected or deleted deal does not move or close again"),
207
- validation("deal.field", "a field of the deal is not valid"),
208
- ["deal.owner-only", "permission_error", "only the owner moves or closes a deal, or makes a pipeline"],
209
- validation("deal.ref", "the reference names no deal"),
206
+ validation("deal.closed", "a deleted deal or lead does not move or close"),
207
+ validation("deal.field", "a field of the deal or lead is not valid"),
208
+ validation("deal.ref", "the reference names no deal or lead"),
210
209
  validation("deal.stage", "no stage of that name, or a stage name more than one pipeline has"),
211
- validation("pipeline.duplicate", "a pipeline of that name is already stored"),
210
+ validation("pipeline.ambiguous", "more than one pipeline has that name; name it by its pipeline: reference"),
211
+ validation("pipeline.duplicate", "a pipeline of that name and kind is already stored"),
212
212
  validation("pipeline.field", "a pipeline needs a name and at least one stage, each named once"),
213
- ["pipeline.not-found", "not_found", "no stored pipeline has that name"],
213
+ ["pipeline.not-found", "not_found", "no stored pipeline has that name or reference"],
214
214
  validation("order.duplicate", "the shop's order with this number is already stored; change its status instead"),
215
215
  validation("order.proposed", "the shop's order with this number already waits for the owner in a findings batch"),
216
216
  validation("order.field", "a field of the order is not valid"),
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { bareCheck, type CheckWords, checkWords } from "./domain/checks.js";
2
2
  export type { CounterField, CounterObservation, CounterObservations, CounterState } from "./domain/counters.js";
3
3
  export { COUNTER_FIELDS, counterValue, observedCounters } from "./domain/counters.js";
4
- export { DEAL_ROLES, DEAL_STATUSES, type DealRole, type DealStatus } from "./domain/deals.js";
4
+ export { DEAL_KINDS, DEAL_ROLES, DEAL_STATUSES, type DealKind, type DealRole, type DealStatus, STAGE_OUTCOMES, type StageOutcome, } from "./domain/deals.js";
5
5
  export { FINANCE_REVIEWS, type FinanceReview, INVOICE_DIRECTIONS, INVOICE_STATUSES, type InvoiceDirection, type InvoiceStatus, MONEY_DIRECTIONS, type MoneyDirection, } from "./domain/finance.js";
6
6
  export { type FormattedText, type HtmlFormatting, type MarkdownFormatting, type TextSpan, validateFormattedText, } from "./domain/formatting.js";
7
7
  export { formatLocator, isLocator, type MessageLocator, parseLocator } from "./domain/locator.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { bareCheck, checkWords } from "./domain/checks.js";
2
2
  export { COUNTER_FIELDS, counterValue, observedCounters } from "./domain/counters.js";
3
- export { DEAL_ROLES, DEAL_STATUSES } from "./domain/deals.js";
3
+ export { DEAL_KINDS, DEAL_ROLES, DEAL_STATUSES, STAGE_OUTCOMES, } from "./domain/deals.js";
4
4
  export { FINANCE_REVIEWS, INVOICE_DIRECTIONS, INVOICE_STATUSES, MONEY_DIRECTIONS, } from "./domain/finance.js";
5
5
  export { validateFormattedText, } from "./domain/formatting.js";
6
6
  export { formatLocator, isLocator, parseLocator } from "./domain/locator.js";
@@ -6,8 +6,9 @@ import * as v from "valibot";
6
6
  import { answerEntity } from "../../cli/answer-entity.js";
7
7
  import { environmentOf, outputFor } from "../../cli/context.js";
8
8
  import { FETCH, LOCAL_READ, REMOTE_READ } from "../../cli/effects.js";
9
- import { itemOf, listOf, renderAnswer, snakeKeys } from "../../cli/envelope.js";
9
+ import { itemOf, listOf, snakeKeys } from "../../cli/envelope.js";
10
10
  import { listQuery, typedQuery, withQuery } from "../../cli/list-query.js";
11
+ import { answerLocally } from "../../cli/local-times.js";
11
12
  import { messagesAnswer, offsetOf, shownOf } from "../../cli/messenger/search-answers.js";
12
13
  import { backendRequest, noteServer, serverSearchOptions } from "../../cli/messenger/search-backend-options.js";
13
14
  import { positiveCount } from "../../cli/paging.js";
@@ -193,7 +194,7 @@ Search guide: https://github.com/WireCatLabs/cli-core/blob/main/docs/search/quer
193
194
  const issues = serverIssues(found.server);
194
195
  if (issues.length)
195
196
  answer.meta = { ...answer.meta, complete: false, issues: [...(answer.meta?.issues ?? []), ...issues] };
196
- renderAnswer(this, output, answer, () => {
197
+ answerLocally(this, output, answer, (local) => {
197
198
  if (!found.wordsReady)
198
199
  output.renderer.note(`the word index is still being built — \`${mail.app.command} store migrate\` finishes it`);
199
200
  noteServer(output, found.server);
@@ -204,12 +205,12 @@ Search guide: https://github.com/WireCatLabs/cli-core/blob/main/docs/search/quer
204
205
  const shown = shownOf(found.items.length, (page - 1) * limit, found.total);
205
206
  if (shown)
206
207
  output.renderer.note(shown);
207
- return found.items.map((item) => `${hitText(item, level)}\n`).join("");
208
- });
208
+ return found.items.map((item) => `${hitText(item, level, local)}\n`).join("");
209
+ }, options.timezone);
209
210
  });
210
211
  return new Command("search").summary("search the stored mail").description("find stored emails").addCommand(leaf);
211
212
  };
212
- const shownText = (emails) => `${emails.map((email) => [hitText(email, "full"), attachmentsText(email)].filter(Boolean).join("\n")).join("\n\n")}\n`;
213
+ const shownText = (emails, local) => `${emails.map((email) => [hitText(email, "full", local), attachmentsText(email)].filter(Boolean).join("\n")).join("\n\n")}\n`;
213
214
  const messagesCommand = (mail) => {
214
215
  const messages = new Command("messages").summary("read stored emails").description("read the emails sync stored");
215
216
  annotate(messages.command("show"), {
@@ -249,9 +250,9 @@ const messagesCommand = (mail) => {
249
250
  });
250
251
  const output = outputFor(this);
251
252
  const shown = emails.map((email) => snakeKeys(atDetail(email)));
252
- renderAnswer(this, output, beforeN || afterN
253
+ answerLocally(this, output, beforeN || afterN
253
254
  ? listOf("messages show", { items: shown, limit: Math.max(1, shown.length), has_more: false })
254
- : itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0], unread ? { meta: { complete: false, issues: [unread] } } : undefined), () => shownText(emails));
255
+ : itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0], unread ? { meta: { complete: false, issues: [unread] } } : undefined), (local) => shownText(emails, local));
255
256
  });
256
257
  messages.addCommand(acknowledgeCommand(mail));
257
258
  messages.addCommand(resetMarkCommand(mail));
@@ -315,14 +316,14 @@ const conversationsCommand = (mail) => {
315
316
  last_email_at: thread.lastEmailAt === null ? null : new Date(thread.lastEmailAt).toISOString(),
316
317
  untrusted: ["subject"],
317
318
  }));
318
- renderAnswer(this, outputFor(this), listOf("conversations list", {
319
+ answerLocally(this, outputFor(this), listOf("conversations list", {
319
320
  items,
320
321
  limit,
321
322
  has_more: threads.length > limit,
322
323
  page,
323
324
  ...(typed.trim() ? { args: { query: typed } } : {}),
324
- }), () => items
325
- .map((one) => `${one.ref}\n ${one.last_email_at ?? "time unknown"} ${one.emails_count} ${one.subject ?? ""}\n`)
325
+ }), (local) => items
326
+ .map((one) => `${one.ref}\n ${one.last_email_at === null ? "time unknown" : local(one.last_email_at)} ${one.emails_count} ${one.subject ?? ""}\n`)
326
327
  .join(""));
327
328
  });
328
329
  answerEntity(list, "mail_thread", mailThread);
@@ -349,7 +350,7 @@ const conversationsCommand = (mail) => {
349
350
  : hit));
350
351
  });
351
352
  const items = emails.map((email) => snakeKeys(atDetail(email)));
352
- renderAnswer(this, outputFor(this), listOf("conversations show", { items, limit: Math.max(1, items.length), has_more: false }), () => shownText(emails));
353
+ answerLocally(this, outputFor(this), listOf("conversations show", { items, limit: Math.max(1, items.length), has_more: false }), (local) => shownText(emails, local));
353
354
  });
354
355
  return conversations;
355
356
  };
@@ -5,8 +5,9 @@ import { defineSteps, MAX_NEXT_STEPS, stepOf, } from "@wirecat/cli-base/standard
5
5
  import { Command } from "commander";
6
6
  import { environmentOf, outputFor } from "../../cli/context.js";
7
7
  import { LOCAL_READ } from "../../cli/effects.js";
8
- import { listOf, renderAnswer } from "../../cli/envelope.js";
8
+ import { listOf } from "../../cli/envelope.js";
9
9
  import { listQuery, nativeField, nativeTerms, queryHelp, typedQuery } from "../../cli/list-query.js";
10
+ import { answerLocally } from "../../cli/local-times.js";
10
11
  import { pointsOf } from "../../cli/messenger/catch-up.js";
11
12
  import { catchUpQuery, SUMMARY_UNTRUSTED, summaryOf } from "../../cli/messenger/catch-up-answers.js";
12
13
  import { messageItem } from "../../cli/messenger/message-items.js";
@@ -231,11 +232,13 @@ const holderReview = async (command, mail, holder, query) => {
231
232
  : []),
232
233
  ];
233
234
  const output = outputFor(command);
234
- renderAnswer(command, output, answer, () => {
235
+ answerLocally(command, output, answer, (local) => {
235
236
  output.renderer.note(read.mark
236
237
  ? `${holder}: past ${read.mark.throughId}${emails.length ? `; findings key ${read.findingsKey}` : ""}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
237
238
  : `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
238
- return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`).join("");
239
+ return emails
240
+ .map((hit) => `${hitText(hit, undefined, local)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`)
241
+ .join("");
239
242
  });
240
243
  };
241
244
  /**
@@ -311,7 +314,7 @@ an agent over MCP, and each scheduled run.`)
311
314
  const answer = reviewAnswer(found, mailbox, saved !== undefined, stepsFor(mail.mount, mailbox));
312
315
  if (meta)
313
316
  answer.meta = { ...meta, ...answer.meta };
314
- renderAnswer(this, output, answer, () => {
317
+ answerLocally(this, output, answer, (local) => {
315
318
  const read = found.chats.flatMap((chat) => chat.messages);
316
319
  if (found.unanswered || found.waiting)
317
320
  output.renderer.note(`${[
@@ -319,16 +322,16 @@ an agent over MCP, and each scheduled run.`)
319
322
  found.waiting ? `waiting ${found.waiting.olderThanHours}h or more` : "",
320
323
  ]
321
324
  .filter(Boolean)
322
- .join(", ")}, threads active from ${found.since}`);
325
+ .join(", ")}, threads active from ${local(found.since)}`);
323
326
  else
324
- output.renderer.note(`from ${found.since} to ${found.until}${found.complete ? "" : ", incomplete"}`);
327
+ output.renderer.note(`from ${local(found.since)} to ${local(found.until)}${found.complete ? "" : ", incomplete"}`);
325
328
  if (found.quiet > 0)
326
329
  output.renderer.note(`bulk:false left out ${found.quiet} threads`);
327
330
  if (read.length === 0)
328
331
  output.renderer.note("nothing new in the stored mail");
329
332
  return read
330
333
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
331
- .map((hit) => `${hitText(hit)}${markText(hit.marks)}\n`)
334
+ .map((hit) => `${hitText(hit, undefined, local)}${markText(hit.marks)}\n`)
332
335
  .join("");
333
336
  });
334
337
  saved?.save(found.checked ?? {}, found.complete ? { through: found.until } : {});
@@ -54,7 +54,7 @@ export declare const showMail: (store: MessageStore, locator: string, { before,
54
54
  */
55
55
  export declare const hitText: (item: StoredHit & {
56
56
  excerpt?: string;
57
- }, level?: "brief" | "full") => string;
57
+ }, level?: "brief" | "full", shown?: (time: string) => string) => string;
58
58
  /** Each attachment of a shown email with its text indented under it, or why it has none. */
59
59
  export declare const attachmentsText: (hit: ShownMail) => string;
60
60
  export {};
package/dist/mail/read.js CHANGED
@@ -82,7 +82,7 @@ const line = (text) => text.replace(/\s+/g, " ").trim();
82
82
  * A found message or email for a reader: its reference first, then when and from whom, who else it went to and
83
83
  * where it is filed, its subject or chat, and an excerpt — the whole text only at `full`.
84
84
  */
85
- export const hitText = (item, level) => {
85
+ export const hitText = (item, level, shown = (time) => time) => {
86
86
  const mail = item.email;
87
87
  const names = (people) => people.map(({ name, address }) => line(name ?? address)).join(", ");
88
88
  const between = mail
@@ -97,7 +97,7 @@ export const hitText = (item, level) => {
97
97
  const who = line(item.senderName ?? item.senderId ?? "unknown");
98
98
  return [
99
99
  item.locator,
100
- ` ${item.timestamp} ${who}${level === "brief" || !between ? "" : ` ${between}`}`,
100
+ ` ${shown(item.timestamp)} ${who}${level === "brief" || !between ? "" : ` ${between}`}`,
101
101
  ...(level === "brief" || !item.chatTitle ? [] : [` ${line(item.chatTitle)}`]),
102
102
  ` ${level === "full" ? item.text.replace(/\n/g, "\n ") : line(item.excerpt ?? item.text)}`,
103
103
  ].join("\n");
@@ -1,7 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { mayDecide } from "../../store/decisions.js";
3
3
  import { actionKind } from "./kinds/action.js";
4
- import { dealKind } from "./kinds/deals.js";
5
4
  import { factKind } from "./kinds/fact.js";
6
5
  import { invoiceKind, paymentKind } from "./kinds/finance.js";
7
6
  import { findingsKind } from "./kinds/findings.js";
@@ -136,7 +135,6 @@ export const proposalRegistry = (store) => {
136
135
  invoiceKind(store),
137
136
  paymentKind(store),
138
137
  orderKind(store),
139
- dealKind(store),
140
138
  organizationKind(store),
141
139
  memoryKind(store),
142
140
  findingsKind(store),
@@ -34,7 +34,7 @@ export type { BotUpdate, BotUpdateStore } from "./sqlite/bot-updates.js";
34
34
  export type { CatalogActor, CatalogEntry, CatalogInput, CatalogQuery, CatalogStore } from "./sqlite/catalog.js";
35
35
  export { CATALOG_QUERY } from "./sqlite/catalog.js";
36
36
  export type { ChatMetadata } from "./sqlite/chats.js";
37
- export type { Deal, DealFilter, DealInput, DealPerson, DealsStore, Pipeline, PipelineStage, } from "./sqlite/deals.js";
37
+ export type { Deal, DealFilter, DealInput, DealPerson, DealsStore, FunnelStage, Pipeline, PipelineInput, PipelineReport, PipelineStage, } from "./sqlite/deals.js";
38
38
  export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
39
39
  export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
40
40
  export type { EvidenceSource } from "./sqlite/evidence-sources.js";