@wirecat/cli-core 0.382.0 → 0.384.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/attachments/documents.d.ts +3 -0
  3. package/dist/attachments/documents.js +17 -0
  4. package/dist/cli/bot/moderation.js +4 -3
  5. package/dist/cli/bot/store.js +5 -4
  6. package/dist/cli/list-query.d.ts +11 -1
  7. package/dist/cli/list-query.js +11 -1
  8. package/dist/cli/meeting-batch.js +2 -1
  9. package/dist/cli/messenger/admin-moderation-command.js +4 -3
  10. package/dist/cli/messenger/archive-commands.js +5 -3
  11. package/dist/cli/messenger/backfill-command.js +9 -4
  12. package/dist/cli/messenger/catch-up-answers.d.ts +5 -2
  13. package/dist/cli/messenger/catch-up-answers.js +11 -6
  14. package/dist/cli/messenger/chat-lists.js +11 -6
  15. package/dist/cli/messenger/chats-stats-command.js +7 -7
  16. package/dist/cli/messenger/contacts-command.js +4 -3
  17. package/dist/cli/messenger/conversation-answers.d.ts +2 -2
  18. package/dist/cli/messenger/conversation-answers.js +5 -2
  19. package/dist/cli/messenger/conversations-command.js +7 -3
  20. package/dist/cli/messenger/inbox.js +2 -2
  21. package/dist/cli/messenger/replies-command.js +4 -3
  22. package/dist/cli/messenger/review.js +2 -1
  23. package/dist/cli/messenger/stats-answers.d.ts +1 -1
  24. package/dist/cli/messenger/stats-answers.js +4 -3
  25. package/dist/cli/runs/search.d.ts +7 -0
  26. package/dist/cli/runs/search.js +11 -3
  27. package/dist/documents/blocks.d.ts +45 -0
  28. package/dist/documents/blocks.js +48 -0
  29. package/dist/documents/index.d.ts +5 -0
  30. package/dist/documents/index.js +4 -0
  31. package/dist/documents/markdown-blocks.d.ts +3 -0
  32. package/dist/documents/markdown-blocks.js +123 -0
  33. package/dist/documents/push-model.d.ts +89 -0
  34. package/dist/documents/push-model.js +1 -0
  35. package/dist/documents.d.ts +1 -0
  36. package/dist/documents.js +1 -0
  37. package/dist/errors/reasons.js +3 -0
  38. package/dist/meetings/answers.d.ts +4 -2
  39. package/dist/meetings/answers.js +2 -1
  40. package/dist/meetings/cli/index.d.ts +1 -1
  41. package/dist/meetings/cli/index.js +1 -1
  42. package/dist/meetings/cli/search.d.ts +5 -0
  43. package/dist/meetings/cli/search.js +23 -5
  44. package/dist/meetings/mcp/search.d.ts +3 -1
  45. package/dist/meetings/mcp/search.js +5 -4
  46. package/dist/services/admin-statistics.d.ts +3 -0
  47. package/dist/services/admin-statistics.js +10 -7
  48. package/dist/services/documents/block-diff.d.ts +24 -0
  49. package/dist/services/documents/block-diff.js +91 -0
  50. package/dist/services/documents/push.d.ts +60 -0
  51. package/dist/services/documents/push.js +218 -0
  52. package/dist/services/retention.d.ts +7 -0
  53. package/dist/services/retention.js +10 -5
  54. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.384.0 — 22.10.2026
11
+
12
+ ### Added
13
+
14
+ - **More commands say how they read a typed date** (WC-TIME-3, WC-TIME-5): `meta.window` gives the UTC bounds they
15
+ used, and `meta.timezone` names the zone a value without its own zone was read in. Now: `moderate` (the bot's and
16
+ the admin's), the bot's `store fetch`, `store export`, `messages sync`, `stats chats show`, `contacts context`,
17
+ `search conversations`, `replies test` (its 7-day default window too), `runs search`, `stats chats retention`, the
18
+ administrator reports (`stats messages unanswered`, `stats contacts responses`, `stats chats newcomers`,
19
+ `stats messages discussion`), `search meetings` (also in `meetings batch`; over MCP only the window, since it takes
20
+ epoch times), and a `time:` or `date:` term in `chats events`, `chats members history`, `inbox` and `review`.
21
+ `search conversations` now reads a date alone in its `--timezone` when one is given. A host of `search meetings`
22
+ passes `meetingSearchMeta(options)` to `meetingHitsList` to report the same.
23
+
24
+ ## 0.383.0 — 22.10.2026
25
+
26
+ ### Added
27
+
28
+ - **`readSpreadsheet(bytes, "xlsx" | "ods")` from `@wirecat/cli-core/documents`** returns a workbook's cells per
29
+ worksheet (values as displayed), the same reader text extraction uses, so a provider can mirror a workbook's tabs.
30
+ - **Push local files into a provider as native documents (`@wirecat/cli-core/documents`).** `pushDocuments` mirrors
31
+ a local folder one way through a provider's `DocumentTarget`: new files are created, changed files updated, and
32
+ unchanged files skipped by content hash with no remote call. Before a create it saves a pending origin key, so a
33
+ crash after the provider wrote finds the file (`findByOrigin`) instead of creating a duplicate. The provider's
34
+ `prepare` compares the remote with the last push before writing; a remote copy changed since then is a conflict
35
+ unless `force`, and only the `write` step passes the guard. A local delete is reported as an
36
+ orphan and nothing is ever deleted remotely. `planPush` is the pure plan behind `dryRun`; `scanLocal` skips hidden
37
+ files, editor leftovers and symbolic links. Every write passes through the caller's `guard` with its own operation
38
+ id. For documents with structure, `markdownBlocks` (async, its parser loaded on first use) parses Markdown (CommonMark and GFM tables, task lists,
39
+ strikethrough) into `Block`s a provider writes natively, `diffBlocks` gives the edits that leave unchanged blocks
40
+ in place, and `reconcileBlocks` lets a checklist tick change on either side without a conflict. See
41
+ [documents](docs/dev/DOCUMENTS.md#push-local-files-to-a-provider).
42
+
8
43
  ## 0.382.0 — 22.10.2026
9
44
 
10
45
  ### Added
@@ -1,6 +1,9 @@
1
1
  import type { Extraction, FileHint } from "../readers/types.js";
2
+ import { type Sheet } from "./sheets.js";
2
3
  export type DocumentKind = "odt" | "ods" | "odp" | "xlsx" | "pptx" | "epub";
3
4
  export declare const documentKind: (hint: FileHint) => DocumentKind | undefined;
4
5
  /** v2 for spreadsheets: rows under their header instead of one line per cell, so older text is read again. */
5
6
  export declare const documentExtractor: (kind: DocumentKind) => string;
7
+ /** The cells of an XLSX or ODS workbook, one `Sheet` per worksheet in workbook order, values as displayed. */
8
+ export declare const readSpreadsheet: (bytes: Uint8Array, kind: "xlsx" | "ods", signal?: AbortSignal) => Promise<Sheet[]>;
6
9
  export declare const readDocument: (bytes: Uint8Array, kind: DocumentKind, signal?: AbortSignal) => Promise<Extraction>;
@@ -393,6 +393,23 @@ const readBook = (archive, output) => {
393
393
  visit(bodies[0]);
394
394
  }
395
395
  };
396
+ /** The cells of an XLSX or ODS workbook, one `Sheet` per worksheet in workbook order, values as displayed. */
397
+ export const readSpreadsheet = async (bytes, kind, signal) => {
398
+ try {
399
+ const archive = await readContainer(bytes, signal);
400
+ const sheets = kind === "xlsx" ? readSheet(archive) : readOdf(archive, kind, new Output());
401
+ if (!sheets)
402
+ throw new Error("wrong_format");
403
+ return sheets;
404
+ }
405
+ catch (error) {
406
+ if (error instanceof CliError)
407
+ throw error;
408
+ throw new CliError("validation_error", `the ${kind.toUpperCase()} workbook could not be read`, {
409
+ reason: "spreadsheet.unreadable",
410
+ });
411
+ }
412
+ };
396
413
  export const readDocument = async (bytes, kind, signal) => {
397
414
  const extractor = documentExtractor(kind);
398
415
  try {
@@ -7,7 +7,7 @@ import { defaultRules, ModerationRules, moderationPathFor, RULE_KEYS } from "../
7
7
  import { moderateWith } from "../../moderation/run.js";
8
8
  import { guardedWrite } from "../../sends/guarded.js";
9
9
  import { newOperationId } from "../../sends/send-id.js";
10
- import { momentOf } from "../../services/moment.js";
10
+ import { dateWindow } from "../../services/moment.js";
11
11
  import { environmentOf } from "../context.js";
12
12
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, ownerOnlyOption, REMOTE_WRITE } from "../effects.js";
13
13
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
@@ -155,6 +155,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
155
155
  // An in-process caller's answer counts under --json too: it is not a terminal that could wait for ever.
156
156
  const given = environmentOf(this).answer;
157
157
  const ask = async (question) => (given ? await given(question) : await answerOf(this, question));
158
+ const window = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
158
159
  await context.run(async (events) => {
159
160
  const adapter = await context.authenticated({ events });
160
161
  const botId = await botIdOf(context, adapter);
@@ -169,7 +170,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
169
170
  commandFor: (group, finding) => finding.action === "delete"
170
171
  ? `${context.words} messages delete ${group} ${finding.messageId} --allow-dangerous`
171
172
  : `${context.words} chats members remove ${group} ${finding.personId}`,
172
- ...(options.sinceTime === undefined ? {} : { since: momentOf(options.sinceTime, "--since-time") }),
173
+ ...(window.since === undefined ? {} : { since: window.since }),
173
174
  dryRun: options.dryRun === true,
174
175
  allowDangerous: options.allowDangerous === true,
175
176
  maxActions,
@@ -188,7 +189,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
188
189
  ...(row.reason ? { why: row.reason } : {}),
189
190
  })));
190
191
  else
191
- renderAnswer(this, context, itemOf({ chat_id: chatId, rows: snakeKeys(rows) }));
192
+ renderAnswer(this, context, itemOf({ chat_id: chatId, rows: snakeKeys(rows) }, { meta: window.meta }));
192
193
  for (const note of notes)
193
194
  context.renderer.note(note);
194
195
  });
@@ -3,9 +3,9 @@ import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { parseDuration } from "../../domain/duration.js";
5
5
  import { fetchInto } from "../../services/archive.js";
6
- import { momentOf } from "../../services/moment.js";
6
+ import { dateWindow } from "../../services/moment.js";
7
7
  import { FETCH } from "../effects.js";
8
- import { renderItem } from "../messenger/chat-answers.js";
8
+ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
9
9
  import { stopOnSignal } from "../messenger/patience.js";
10
10
  import { botContext, online } from "./context.js";
11
11
  import { botCan, botIdOf } from "./messages.js";
@@ -37,7 +37,8 @@ export const botStoreCommand = (bot, fetching) => {
37
37
  throw new CliError("validation_error", "give --since-time or --last, not both: how far back the fetch goes");
38
38
  }
39
39
  const pauseMs = parseDuration(options.pause, "--pause");
40
- const sinceMs = options.sinceTime === undefined ? undefined : momentOf(options.sinceTime, "--since-time");
40
+ const bounds = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
41
+ const sinceMs = bounds.since;
41
42
  const context = online(botContext(this, bot), this);
42
43
  const ref = context.chatRef(chat);
43
44
  const stop = stopOnSignal(this);
@@ -73,7 +74,7 @@ export const botStoreCommand = (bot, fetching) => {
73
74
  stop: stop.signal,
74
75
  onPage: () => { },
75
76
  }));
76
- renderItem(this, context, result);
77
+ renderAnswer(this, context, itemOf(snakeKeys(result), { meta: bounds.meta }));
77
78
  }
78
79
  finally {
79
80
  await adapter.close();
@@ -44,10 +44,20 @@ export declare const nativeTerms: (ast: QueryAst | undefined, command: string) =
44
44
  * as validation already accepts them. A range or wildcard is refused rather than read as a value.
45
45
  */
46
46
  export declare const queryTerms: (words: QueryWords, registry: QueryRegistry, command: string) => Map<string, string>;
47
- /** A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`. */
47
+ /**
48
+ * A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`, and the `meta`
49
+ * an answer reports them by: the zone calendar words and bare dates were read in, and the bounds in UTC.
50
+ */
48
51
  export declare const timeBounds: (term: Predicate | undefined, zone?: string) => {
49
52
  since?: number;
50
53
  until?: number;
54
+ meta?: {
55
+ timezone: string;
56
+ window: {
57
+ since?: string;
58
+ until?: string;
59
+ };
60
+ };
51
61
  };
52
62
  /** A text term as the plain words a provider searches by: `name:ana`, `name:"Ana K"`, `name:*ana*`. */
53
63
  export declare const searchText: (term: Predicate | undefined) => string | undefined;
@@ -130,7 +130,10 @@ export const queryTerms = (words, registry, command) => {
130
130
  }
131
131
  return terms;
132
132
  };
133
- /** A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`. */
133
+ /**
134
+ * A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`, and the `meta`
135
+ * an answer reports them by: the zone calendar words and bare dates were read in, and the bounds in UTC.
136
+ */
134
137
  export const timeBounds = (term, zone = timezoneOf()) => {
135
138
  if (!term)
136
139
  return {};
@@ -139,6 +142,13 @@ export const timeBounds = (term, zone = timezoneOf()) => {
139
142
  return {
140
143
  ...(range.lower === undefined ? {} : { since: range.lower }),
141
144
  ...(range.upper === undefined ? {} : { until: range.upper }),
145
+ meta: {
146
+ timezone: zone,
147
+ window: {
148
+ ...(range.lower === undefined ? {} : { since: new Date(range.lower).toISOString() }),
149
+ ...(range.upper === undefined ? {} : { until: new Date(range.upper).toISOString() }),
150
+ },
151
+ },
142
152
  };
143
153
  };
144
154
  /** A text term as the plain words a provider searches by: `name:ana`, `name:"Ana K"`, `name:*ana*`. */
@@ -2,7 +2,7 @@ import { BATCH_LIMITS, CliError, prepareBatch, runBatch, } from "@wirecat/cli-ba
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command, Option } from "commander";
4
4
  import { failureContext } from "../errors/reasons.js";
5
- import { createMeetingPassageCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingPassageRequest, meetingSearchArguments, } from "../meetings/cli/index.js";
5
+ import { createMeetingPassageCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingPassageRequest, meetingSearchArguments, meetingSearchMeta, } from "../meetings/cli/index.js";
6
6
  import { meetingHitsList, parseMeetingReference, } from "../meetings/index.js";
7
7
  import { formatMeetingEvidenceText, formatMeetingPassage } from "../services/meeting-passage-format.js";
8
8
  import { itemOf, listOf } from "./envelope.js";
@@ -48,6 +48,7 @@ const handlers = (reader, meta) => {
48
48
  ...meetingHitsList(found, request.limit ?? 100, "search meetings", meetingSearchArguments(query, flags)),
49
49
  meta: {
50
50
  ...meta(),
51
+ ...meetingSearchMeta(flags),
51
52
  complete,
52
53
  ...(!complete ? { issues: [{ code: "invalid_response", source: "meeting-index" }] } : {}),
53
54
  },
@@ -3,7 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { describe, MAX_ACTIONS } from "../../moderation/check.js";
5
5
  import { RULE_KEYS } from "../../moderation/rules.js";
6
- import { momentOf } from "../../services/moment.js";
6
+ import { dateWindow } from "../../services/moment.js";
7
7
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
8
8
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
9
9
  import { answerOf } from "./ask.js";
@@ -57,8 +57,9 @@ export const moderateCommand = (messenger) => annotate(new Command("moderate"),
57
57
  if (!Number.isInteger(maxActions) || maxActions < 0) {
58
58
  throw new CliError("validation_error", `--max-actions takes a whole number — got ${String(options.maxActions)}`);
59
59
  }
60
+ const window = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
60
61
  const { chatId, rows, notes } = await context.withServices((services) => services.moderation.moderate(chat, {
61
- ...(options.sinceTime === undefined ? {} : { since: momentOf(options.sinceTime, "--since-time") }),
62
+ ...(window.since === undefined ? {} : { since: window.since }),
62
63
  dryRun: options.dryRun === true,
63
64
  allowDangerous: options.allowDangerous === true,
64
65
  maxActions,
@@ -69,7 +70,7 @@ export const moderateCommand = (messenger) => annotate(new Command("moderate"),
69
70
  return /^\s*y(es)?\s*$/i.test(answer);
70
71
  },
71
72
  }));
72
- renderAnswer(this, context, itemOf(snakeKeys({ chatId, rows })), () => rows
73
+ renderAnswer(this, context, itemOf(snakeKeys({ chatId, rows }), { meta: window.meta }), () => rows
73
74
  .map((row) => [row.kind, row.personName ?? row.personId, row.rule, row.action, row.outcome, row.reason ?? ""]
74
75
  .join(" ")
75
76
  .trimEnd())
@@ -9,7 +9,7 @@ import { renderMessages } from "../../render/messages.js";
9
9
  import { seal } from "../../sealed.js";
10
10
  import { CHAT_KINDS } from "../../services/chats.js";
11
11
  import { kindsOf } from "../../services/inbox.js";
12
- import { momentOf } from "../../services/moment.js";
12
+ import { dateWindow } from "../../services/moment.js";
13
13
  import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
14
14
  import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
15
15
  import { jobsCommand } from "./backfill-command.js";
@@ -185,7 +185,8 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
185
185
  }
186
186
  const password = encrypt ? await passwordOf(this, { twice: true }) : undefined;
187
187
  const chat = chats[0];
188
- const from = since === undefined ? undefined : new Date(momentOf(since, "--since-time")).toISOString();
188
+ const window = dateWindow(since === undefined ? {} : { sinceTime: since });
189
+ const from = window.since === undefined ? undefined : new Date(window.since).toISOString();
189
190
  const { account, title, messages } = await context.withServices((services) => services.archive.export(chat, from === undefined ? {} : { since: from }));
190
191
  if (output !== undefined) {
191
192
  const path = resolve(output);
@@ -210,7 +211,7 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
210
211
  format: format ?? "jsonl",
211
212
  count: messages.length,
212
213
  ...(password === undefined ? {} : { encrypted: true }),
213
- }));
214
+ }, { meta: window.meta }));
214
215
  return;
215
216
  }
216
217
  if (format === "markdown")
@@ -223,6 +224,7 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
223
224
  items: messageItems(messages, account),
224
225
  limit: Math.max(1, messages.length),
225
226
  has_more: false,
227
+ meta: window.meta,
226
228
  }));
227
229
  else
228
230
  context.streams.data(renderMessages(messages, {
@@ -9,7 +9,7 @@ import { isCliFailure } from "../../errors/failures.js";
9
9
  import { envName } from "../../ports/app.js";
10
10
  import { FETCHING } from "../../services/archive.js";
11
11
  import { JOBS_QUERY, jobItems } from "../../services/job-query.js";
12
- import { momentOf } from "../../services/moment.js";
12
+ import { dateWindow } from "../../services/moment.js";
13
13
  import { validateCatchUpBounds } from "../../services/search-catchup.js";
14
14
  import { applyTaskRulesSinceLastPass } from "../../services/task-rules.js";
15
15
  import { environmentOf } from "../context.js";
@@ -72,8 +72,9 @@ export const syncCommand = (messenger) => {
72
72
  }
73
73
  const limit = sizes.limit ?? fetching.maxPages * fetching.page;
74
74
  const pauseMs = parseDuration(pause, "--pause");
75
- const window = since ?? (all && last === undefined ? ALL_SINCE : undefined);
76
- const sinceMs = window === undefined ? undefined : momentOf(window, "--since-time");
75
+ const from = since ?? (all && last === undefined ? ALL_SINCE : undefined);
76
+ const window = dateWindow(from === undefined ? {} : { sinceTime: from });
77
+ const sinceMs = window.since;
77
78
  const context = messengerContext(this, messenger);
78
79
  const prepare = catchUp ?? context.settings.searchCatchUp ?? false;
79
80
  const preparation = {
@@ -123,7 +124,10 @@ export const syncCommand = (messenger) => {
123
124
  pageSize,
124
125
  ...(last === undefined ? {} : { last }),
125
126
  });
126
- renderAnswer(this, context, itemOf(snakeKeys(started), { next: [nextStep("store.job", { args: { job: started.job } })] }));
127
+ renderAnswer(this, context, itemOf(snakeKeys(started), {
128
+ next: [nextStep("store.job", { args: { job: started.job } })],
129
+ meta: window.meta,
130
+ }));
127
131
  return;
128
132
  }
129
133
  const jobs = jobsDir(messenger.app, context.env);
@@ -164,6 +168,7 @@ export const syncCommand = (messenger) => {
164
168
  : []),
165
169
  ...(toReview > 0 ? [nextStep("mentions.review", { args: { new: true }, values: { shown } })] : []),
166
170
  ],
171
+ meta: window.meta,
167
172
  }));
168
173
  if (tasks?.held)
169
174
  context.renderer.note(`the first sync left ${tasks.held} possible task(s) in ${tasks.heldChats ?? 0} chat(s) unopened ` +
@@ -1,3 +1,4 @@
1
+ import type { Meta } from "@wirecat/cli-base/standards";
1
2
  import type { Inbox, Review } from "../../domain/models.js";
2
3
  import { type Hearing } from "../../speech/hearing.js";
3
4
  import type { AccountKey } from "../../store/store.js";
@@ -10,8 +11,9 @@ export declare const REVIEW_QUERY: import("@wirecat/cli-base/query").QueryRegist
10
11
  export declare const catchUpQuery: (words: QueryWords, registry: typeof INBOX_QUERY, command: string) => {
11
12
  since?: number;
12
13
  chat?: string;
14
+ meta?: Partial<Meta>;
13
15
  };
14
- export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean) => import("../envelope.js").ListBody<{
16
+ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
15
17
  ref: string;
16
18
  chat_id: string;
17
19
  chat_title: string | null | undefined;
@@ -51,10 +53,11 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
51
53
  * A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
52
54
  * Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
53
55
  */
54
- export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined, reader: AccountKey, { transcribe, fresh, chat }: {
56
+ export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined, reader: AccountKey, { transcribe, fresh, chat, meta }: {
55
57
  transcribe: boolean;
56
58
  fresh: boolean;
57
59
  chat?: string;
60
+ meta?: Partial<Meta>;
58
61
  }) => import("../envelope.js").ListBody<{
59
62
  ref: string;
60
63
  chat_id: string;
@@ -17,13 +17,17 @@ export const REVIEW_QUERY = defineQuery({
17
17
  export const catchUpQuery = (words, registry, command) => {
18
18
  const terms = nativeTerms(listQuery(typedQuery(words), registry, command), command);
19
19
  const term = terms.get("date");
20
- const { since, until } = timeBounds(term);
20
+ const { since, until, meta } = timeBounds(term);
21
21
  if (term && (until !== undefined || since === undefined))
22
22
  queryError("query.unsupported-operator", term.span, `${command} reads from a point on: date>2h, not an end`, {
23
23
  command,
24
24
  });
25
25
  const chat = terms.get("chat")?.value;
26
- return { ...(since === undefined ? {} : { since }), ...(chat === undefined ? {} : { chat }) };
26
+ return {
27
+ ...(since === undefined ? {} : { since }),
28
+ ...(chat === undefined ? {} : { chat }),
29
+ ...(meta === undefined ? {} : { meta }),
30
+ };
27
31
  };
28
32
  const summaryOf = (message, reader) => {
29
33
  const { ref, chat_id, chat_title, timestamp, sender_name, outgoing, text, transcript, attachments } = messageItem(message, reader);
@@ -44,7 +48,7 @@ const nameOf = (chat) => chat.title ?? chat.id;
44
48
  * What every catch-up answer shares: the messages, oldest first, each naming its chat; what was left out as
45
49
  * `missing`; and a chat cut short as a `messages.list` step, after the steps the command puts first.
46
50
  */
47
- const catchUpList = (command, found, hearing, reader, { transcribe, first = [] }) => {
51
+ const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {} }) => {
48
52
  const read = found.chats
49
53
  .flatMap((chat) => heard(chat.messages, hearing).map((message) => ({ ...message, chatTitle: chat.title })))
50
54
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
@@ -73,16 +77,16 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [] }
73
77
  has_more: false,
74
78
  next: [...first, ...cut.map((chat) => nextStep("messages.list", { args: { chat: chat.id } }))],
75
79
  missing,
76
- ...(found.partial ? { meta: { truncated: true } } : {}),
80
+ meta: { ...meta, ...(found.partial ? { truncated: true } : {}) },
77
81
  });
78
82
  };
79
- export const inboxAnswer = (inbox, hearing, reader, transcribe) => catchUpList("inbox", inbox, hearing, reader, { transcribe });
83
+ export const inboxAnswer = (inbox, hearing, reader, transcribe, meta = {}) => catchUpList("inbox", inbox, hearing, reader, { transcribe, meta });
80
84
  const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
81
85
  /**
82
86
  * A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
83
87
  * Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
84
88
  */
85
- export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat }) => {
89
+ export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat, meta }) => {
86
90
  const continues = !fresh && found.unanswered === undefined;
87
91
  const query = [
88
92
  `date>${quoted(found.complete ? found.until : found.since)}`,
@@ -91,5 +95,6 @@ export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat }
91
95
  return catchUpList("review", found, hearing, reader, {
92
96
  transcribe,
93
97
  first: continues ? [nextStep("review.next", { args: { query: query.join(" ") } })] : [],
98
+ ...(meta === undefined ? {} : { meta }),
94
99
  });
95
100
  };
@@ -18,9 +18,11 @@ export const chatsList = async (services, query, paging) => {
18
18
  };
19
19
  export const chatEvents = async (services, chat, query) => {
20
20
  const terms = nativeTerms(listQuery(typedQuery(query), EVENTS_QUERY, "chats events"), "chats events");
21
- const { since } = timeBounds(terms.get("time"));
21
+ const { since, meta } = timeBounds(terms.get("time"));
22
22
  const found = await services.chats.events(chat, since === undefined ? {} : { since });
23
- return wholeList("chats events", found.events.map((one) => chatEventAnswer(found.chatId, one)), found.more ? { meta: { truncated: true } } : {});
23
+ return wholeList("chats events", found.events.map((one) => chatEventAnswer(found.chatId, one)), {
24
+ meta: { ...meta, ...(found.more ? { truncated: true } : {}) },
25
+ });
24
26
  };
25
27
  export const membersList = async (services, chat, paging) => {
26
28
  const { limit, page, all } = paging;
@@ -33,11 +35,14 @@ export const membersList = async (services, chat, paging) => {
33
35
  };
34
36
  export const membersHistory = async (services, chat, query) => {
35
37
  const terms = nativeTerms(listQuery(typedQuery(query), HISTORY_QUERY, "chats members history"), "chats members history");
36
- const { since } = timeBounds(terms.get("time"));
38
+ const { since, meta } = timeBounds(terms.get("time"));
37
39
  const found = await services.chats.memberHistory(chat, since === undefined ? {} : { since });
38
- return wholeList("chats members history", found.events.map((one) => memberEventAnswer(found.chatId, one)), found.events.length === 0
39
- ? { next: [nextStep("chats.members-fetch", { args: { chat: found.chatId, track: true } })] }
40
- : {});
40
+ return wholeList("chats members history", found.events.map((one) => memberEventAnswer(found.chatId, one)), {
41
+ ...(found.events.length === 0
42
+ ? { next: [nextStep("chats.members-fetch", { args: { chat: found.chatId, track: true } })] }
43
+ : {}),
44
+ ...(meta ? { meta } : {}),
45
+ });
41
46
  };
42
47
  export const membersAudit = async (services, chat, query, options) => {
43
48
  const minScore = scoreFloor(listQuery(typedQuery(query), AUDIT_QUERY, "chats members audit"));
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { EVENTS_DAYS } from "../../services/chats.js";
5
- import { momentOf } from "../../services/moment.js";
5
+ import { dateWindow } from "../../services/moment.js";
6
6
  import { REMOTE_READ } from "../effects.js";
7
7
  import { renderAnswer } from "../envelope.js";
8
8
  import { messengerContext } from "./context.js";
@@ -19,13 +19,13 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
19
19
  const context = messengerContext(this, messenger);
20
20
  const { sinceTime, by, timezone } = this.opts();
21
21
  refuseJsonl(context.format);
22
+ const window = dateWindow({
23
+ ...(sinceTime === undefined ? {} : { sinceTime }),
24
+ ...(timezone === undefined ? {} : { zone: timezone }),
25
+ });
22
26
  const { stats, account } = await context.withServices(async (services) => ({
23
27
  stats: await services.chats.stats(chat, {
24
- ...(sinceTime === undefined
25
- ? {}
26
- : {
27
- since: momentOf(sinceTime, "--since-time", Date.now(), timezone === undefined ? {} : { zone: timezone }),
28
- }),
28
+ ...(window.since === undefined ? {} : { since: window.since }),
29
29
  ...(by ? { by } : {}),
30
30
  ...(timezone === undefined ? {} : { timezone }),
31
31
  }),
@@ -37,7 +37,7 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
37
37
  context.renderer.note("the messenger stopped reading joins and leaves before the period's start — those are lower bounds");
38
38
  if (!stats.members && !stats.membersUnrecorded)
39
39
  context.renderer.note("joins and leaves were not asked of the messenger");
40
- renderAnswer(this, context, chatStatsAnswer(stats, account), () => view(stats));
40
+ renderAnswer(this, context, chatStatsAnswer(stats, account, window.meta), () => view(stats));
41
41
  });
42
42
  const view = (stats) => {
43
43
  const { questions, members } = stats;
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
5
5
  import { levelFor } from "../../sends/permissions.js";
6
6
  import { phoneOf } from "../../services/index.js";
7
- import { dateWindow, momentOf } from "../../services/moment.js";
7
+ import { dateWindow } from "../../services/moment.js";
8
8
  import { maskedAccount } from "../../services/people.js";
9
9
  import { CHAT_MESSAGES, CONTEXT_BYTES, CONTEXT_MESSAGES } from "../../services/person-context.js";
10
10
  import { SCOPES, TIMELINE_ITEMS } from "../../services/person-timeline.js";
@@ -119,10 +119,11 @@ export const contactsCommand = (messenger) => {
119
119
  }));
120
120
  return;
121
121
  }
122
+ const window = dateWindow(sinceTime === undefined ? {} : { sinceTime });
122
123
  const found = await context.withServices((services) => services.people.context(person, {
123
124
  messages: limit ?? CONTEXT_MESSAGES,
124
125
  bytes: CONTEXT_BYTES,
125
- ...(sinceTime === undefined ? {} : { since: momentOf(sinceTime, "--since-time") }),
126
+ ...(window.since === undefined ? {} : { since: window.since }),
126
127
  }));
127
128
  const { hasMore, ...rest } = found;
128
129
  renderAnswer(this, context, itemOf(snakeKeys(rest), {
@@ -131,7 +132,7 @@ export const contactsCommand = (messenger) => {
131
132
  .filter((chat) => chat.provider === messenger.provider)
132
133
  .map((chat) => nextStep("messages.sync", { args: { chat: chat.chatId } }))
133
134
  : [],
134
- ...(hasMore ? { meta: { truncated: true } } : {}),
135
+ meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
135
136
  }));
136
137
  if (hasMore)
137
138
  context.renderer.note("cut at --limit; a larger one shows more");
@@ -1,4 +1,4 @@
1
- import type { Missing, NextStep } from "@wirecat/cli-base/standards";
1
+ import type { Meta, Missing, NextStep } from "@wirecat/cli-base/standards";
2
2
  import type { Message } from "../../domain/models.js";
3
3
  import { type MessengerTraits } from "../../ports/traits.js";
4
4
  import type { ChatReadiness, FoundConversations, Refreshed } from "../../services/embeddings.js";
@@ -11,7 +11,7 @@ export declare const readinessGaps: ({ meaning, model, readiness, refreshed, cov
11
11
  missing: Missing[];
12
12
  };
13
13
  /** `search conversations` and `conversations related`, CLI and MCP alike. */
14
- export declare const foundConversations: (command: string, found: Readiness & Pick<FoundConversations, "hits" | "accounts">, limit: number, messenger: Syncing, chat?: string) => import("../envelope.js").ListBody<unknown>;
14
+ export declare const foundConversations: (command: string, found: Readiness & Pick<FoundConversations, "hits" | "accounts">, limit: number, messenger: Syncing, chat?: string, meta?: Partial<Meta>) => import("../envelope.js").ListBody<unknown>;
15
15
  /** `conversations status`: each built chat with the model it was measured by; group chats never built are missing. */
16
16
  export declare const readinessList: ({ model, chats, unbuiltGroups, }: {
17
17
  model: string;
@@ -53,12 +53,15 @@ export const readinessGaps = ({ meaning, model, readiness, refreshed, coverage }
53
53
  return { next, missing };
54
54
  };
55
55
  /** `search conversations` and `conversations related`, CLI and MCP alike. */
56
- export const foundConversations = (command, found, limit, messenger, chat) => listOf(command, {
56
+ export const foundConversations = (command, found, limit, messenger, chat, meta = {}) => listOf(command, {
57
57
  items: found.hits.map((hit) => snakeKeys(hit)),
58
58
  limit,
59
59
  has_more: false,
60
60
  ...readinessGaps(found, messenger, chat),
61
- ...(found.accounts ? { meta: { accounts: found.accounts.map((one) => `${one.provider}:${one.account}`) } } : {}),
61
+ meta: {
62
+ ...meta,
63
+ ...(found.accounts ? { accounts: found.accounts.map((one) => `${one.provider}:${one.account}`) } : {}),
64
+ },
62
65
  });
63
66
  /** `conversations status`: each built chat with the model it was measured by; group chats never built are missing. */
64
67
  export const readinessList = ({ model, chats, unbuiltGroups, }) => listOf("conversations status", {
@@ -9,7 +9,7 @@ import { renderMessages } from "../../render/messages.js";
9
9
  import { BATCH_SIZE, CONVERSATION_QUERY } from "../../services/conversations.js";
10
10
  import { REFRESH_BOUNDS, } from "../../services/embeddings.js";
11
11
  import { servicesFor, storedDeps } from "../../services/index.js";
12
- import { momentOf } from "../../services/moment.js";
12
+ import { dateWindow } from "../../services/moment.js";
13
13
  import { analysisChoice } from "../analysis-choice.js";
14
14
  import { destroysNothing, LOCAL_READ, LOCAL_WRITE, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "../effects.js";
15
15
  import { embeddingChoice, refuseRemoteQuery } from "../embedding-choice.js";
@@ -531,6 +531,10 @@ export const conversationsSearchCommand = (messenger) => syncOptions(boundOption
531
531
  refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
532
532
  }
533
533
  const { limit } = context.settings;
534
+ const window = dateWindow({
535
+ ...(since === undefined ? {} : { sinceTime: since }),
536
+ ...(options.timezone === undefined ? {} : { zone: options.timezone }),
537
+ });
534
538
  const { found, refreshed } = await context.withServices(async (services) => {
535
539
  const syncing = syncRequest(this, context);
536
540
  const found = await services.embeddings.search(query, {
@@ -551,14 +555,14 @@ export const conversationsSearchCommand = (messenger) => syncOptions(boundOption
551
555
  ...(options.timezone === undefined ? {} : { timezone: options.timezone }),
552
556
  ...(chat === undefined ? {} : { chat }),
553
557
  model,
554
- ...(since === undefined ? {} : { since: new Date(momentOf(since, "--since-time")).toISOString() }),
558
+ ...(window.since === undefined ? {} : { since: new Date(window.since).toISOString() }),
555
559
  });
556
560
  return { found, refreshed: found.prepared };
557
561
  });
558
562
  if (refreshed)
559
563
  for (const note of leftNotes(refreshed, rootOf(this)))
560
564
  context.renderer.note(note);
561
- renderAnswer(this, context, foundConversations("search conversations", found, limit, messenger, chat), () => found.hits.map((hit) => `${hitLine(hit)}\n`).join(""));
565
+ renderAnswer(this, context, foundConversations("search conversations", found, limit, messenger, chat, window.meta), () => found.hits.map((hit) => `${hitLine(hit)}\n`).join(""));
562
566
  if (found.hits.length === 0 && context.format === "pretty")
563
567
  context.renderer.note(found.meaning === "unavailable"
564
568
  ? "nothing matches in words"
@@ -51,7 +51,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
51
51
  const { app } = messenger;
52
52
  const { settings, renderer, format, env } = context;
53
53
  const kinds = kind === undefined ? undefined : kindsOf(kind);
54
- const { since } = catchUpQuery(query, INBOX_QUERY, "inbox");
54
+ const { since, meta } = catchUpQuery(query, INBOX_QUERY, "inbox");
55
55
  if (fresh && since !== undefined)
56
56
  throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
57
57
  const saved = fresh && since === undefined
@@ -83,7 +83,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
83
83
  renderer.note(`marked ${marked.length} chat(s) read up to the newest message shown`);
84
84
  if (inbox.partial && format === "pretty")
85
85
  renderer.warn(PARTIAL_NOTE);
86
- const answer = inboxAnswer(inbox, hearing, reader, transcribe === true);
86
+ const answer = inboxAnswer(inbox, hearing, reader, transcribe === true, meta);
87
87
  renderAnswer(this, context, answer, () => {
88
88
  if (answer.items.length === 0) {
89
89
  renderer.note(inbox.mode === "unread" ? "nothing unread" : `nothing new since ${inbox.since}`);
@@ -6,7 +6,7 @@ import { replyRenderer } from "../../replies/rendering.js";
6
6
  import { audienceWarnings, readReplies, repliesPathFor } from "../../replies/rules.js";
7
7
  import { paused, readRepliesState, repliesStatePathFor, writeRepliesState } from "../../replies/state.js";
8
8
  import { levelFor } from "../../sends/permissions.js";
9
- import { momentOf } from "../../services/moment.js";
9
+ import { dateWindow } from "../../services/moment.js";
10
10
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
11
11
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
12
12
  import { messengerContext } from "./context.js";
@@ -37,7 +37,8 @@ export const repliesCommand = (messenger) => {
37
37
  throw new CliError("validation_error", "--ai calls a model and cannot be combined with --offline");
38
38
  if (ai && levelFor(settings.permissions, "messages.list").level === "deny")
39
39
  throw new CliError("permission_error", "this profile denies reading messages — no stored data was sent to a reply model");
40
- const since = momentOf(sinceTime ?? `${DRY_RUN_DAYS}d`, "--since-time");
40
+ const window = dateWindow({ sinceTime: sinceTime ?? `${DRY_RUN_DAYS}d` });
41
+ const since = window.since;
41
42
  const path = repliesPathFor(messenger.app, settings.profile, env);
42
43
  const { rules, audience } = readReplies(path, messenger.provider);
43
44
  if (rules.length === 0) {
@@ -81,7 +82,7 @@ export const repliesCommand = (messenger) => {
81
82
  .sort(([, a], [, b]) => b - a)
82
83
  .map(([why, count]) => ` passed over ${count}: ${why}`),
83
84
  ]);
84
- renderAnswer(this, context, itemOf(answer), () => `${lines.join("\n")}\n`);
85
+ renderAnswer(this, context, itemOf(answer, { meta: window.meta }), () => `${lines.join("\n")}\n`);
85
86
  });
86
87
  const switched = (on) => async function () {
87
88
  const context = messengerContext(this, messenger);