@wirecat/cli-core 0.563.0 → 0.565.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 (143) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/calendar/busy.d.ts +10 -10
  3. package/dist/calendar/busy.js +9 -9
  4. package/dist/calendar/cli/answers.d.ts +7 -8
  5. package/dist/calendar/cli/answers.js +4 -5
  6. package/dist/calendar/cli/bodies.d.ts +33 -0
  7. package/dist/calendar/cli/bodies.js +52 -0
  8. package/dist/calendar/cli/files.d.ts +8 -0
  9. package/dist/calendar/cli/files.js +41 -0
  10. package/dist/calendar/cli/index.d.ts +58 -51
  11. package/dist/calendar/cli/index.js +311 -284
  12. package/dist/calendar/cli/remind.d.ts +2 -11
  13. package/dist/calendar/cli/remind.js +81 -81
  14. package/dist/calendar/cli/search-all.d.ts +3 -8
  15. package/dist/calendar/cli/search-all.js +47 -42
  16. package/dist/calendar/mcp/index.d.ts +1 -1
  17. package/dist/calendar/prepare.d.ts +1 -1
  18. package/dist/calendar/testing/answers.d.ts +27 -5
  19. package/dist/calendar/testing/answers.js +88 -17
  20. package/dist/calendar/testing/writer.d.ts +1 -0
  21. package/dist/calendar/testing/writer.js +6 -0
  22. package/dist/calendar/writer.d.ts +6 -1
  23. package/dist/calendar/writes.d.ts +3 -1
  24. package/dist/calendar/writes.js +42 -20
  25. package/dist/cli/config-command.js +1 -1
  26. package/dist/cli/config-issues.d.ts +3 -1
  27. package/dist/cli/config-issues.js +2 -2
  28. package/dist/cli/contract-evidence.d.ts +1 -1
  29. package/dist/cli/early-refusal.d.ts +2 -1
  30. package/dist/cli/early-refusal.js +4 -2
  31. package/dist/cli/entry.d.ts +6 -2
  32. package/dist/cli/entry.js +4 -1
  33. package/dist/cli/envelope.d.ts +2 -2
  34. package/dist/cli/index.d.ts +1 -0
  35. package/dist/cli/index.js +1 -0
  36. package/dist/cli/messenger/archive-commands.js +17 -0
  37. package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
  38. package/dist/cli/messenger/catch-up-answers.js +3 -0
  39. package/dist/cli/messenger/search-answers.d.ts +1 -0
  40. package/dist/cli/messenger/search-answers.js +1 -1
  41. package/dist/cli/messenger/stats-answers.d.ts +3 -3
  42. package/dist/cli/messenger/stats-answers.js +21 -9
  43. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  44. package/dist/cli/messenger/tasks-command.js +35 -14
  45. package/dist/cli/permission-keys.d.ts +3 -1
  46. package/dist/cli/permission-keys.js +2 -1
  47. package/dist/cli/preview.d.ts +12 -0
  48. package/dist/cli/preview.js +8 -1
  49. package/dist/cli/settings.d.ts +2 -0
  50. package/dist/cli/settings.js +8 -1
  51. package/dist/conversations/mail-text.d.ts +6 -0
  52. package/dist/conversations/mail-text.js +73 -0
  53. package/dist/domain/calendar.d.ts +2 -0
  54. package/dist/domain/calendar.js +6 -0
  55. package/dist/entities/chat-stats.d.ts +16 -2
  56. package/dist/entities/chat-stats.js +18 -3
  57. package/dist/entities/index.d.ts +50 -802
  58. package/dist/entities/index.js +23 -16
  59. package/dist/entities/mail-counts.d.ts +15 -0
  60. package/dist/entities/mail-counts.js +16 -0
  61. package/dist/entities/meeting.d.ts +1 -1
  62. package/dist/entities/meeting.js +1 -1
  63. package/dist/entities/store-records.d.ts +8 -0
  64. package/dist/entities/store-records.js +8 -0
  65. package/dist/errors/reasons.js +19 -1
  66. package/dist/mail/cli/index.d.ts +5 -0
  67. package/dist/mail/cli/index.js +27 -3
  68. package/dist/mail/cli/review.js +11 -4
  69. package/dist/mail/index.d.ts +2 -0
  70. package/dist/mail/index.js +2 -0
  71. package/dist/mail/stats.d.ts +50 -0
  72. package/dist/mail/stats.js +60 -0
  73. package/dist/mail/whole.d.ts +4 -4
  74. package/dist/mail/whole.js +4 -3
  75. package/dist/mcp/meeting-preparation.d.ts +1 -1
  76. package/dist/mcp/meeting-project-candidates.d.ts +1 -1
  77. package/dist/mcp/meeting-series-inventory.d.ts +1 -1
  78. package/dist/meetings/index.d.ts +2 -2
  79. package/dist/meetings/index.js +2 -2
  80. package/dist/meetings/mcp/index.d.ts +15 -15
  81. package/dist/meetings/mcp/passage.d.ts +1 -1
  82. package/dist/meetings/mcp/project-evidence.d.ts +1 -1
  83. package/dist/meetings/mcp/search.d.ts +1 -1
  84. package/dist/meetings/read-answers.d.ts +22 -1
  85. package/dist/meetings/read-answers.js +38 -0
  86. package/dist/meetings/stats.d.ts +44 -0
  87. package/dist/meetings/stats.js +77 -7
  88. package/dist/search/lucene/registry.js +2 -1
  89. package/dist/search/question-plan.d.ts +16 -0
  90. package/dist/search/question-plan.js +34 -0
  91. package/dist/services/events-search.d.ts +5 -1
  92. package/dist/services/events-search.js +10 -4
  93. package/dist/services/index.d.ts +1 -1
  94. package/dist/services/index.js +1 -1
  95. package/dist/services/meeting-stats.d.ts +12 -1
  96. package/dist/services/meeting-stats.js +12 -0
  97. package/dist/services/messages-discovery.d.ts +1 -5
  98. package/dist/services/messages-discovery.js +5 -18
  99. package/dist/services/messages-meaning.js +25 -3
  100. package/dist/services/messages.js +4 -3
  101. package/dist/services/metadata.d.ts +1 -1
  102. package/dist/services/notes-search.d.ts +5 -1
  103. package/dist/services/notes-search.js +16 -5
  104. package/dist/services/proposals/ids.d.ts +2 -2
  105. package/dist/services/proposals/ids.js +5 -3
  106. package/dist/services/proposals/kinds/findings.d.ts +1 -1
  107. package/dist/services/proposals/kinds/findings.js +1 -1
  108. package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
  109. package/dist/services/proposals/kinds/meeting-findings.js +73 -0
  110. package/dist/services/proposals/kinds/task-close.d.ts +7 -0
  111. package/dist/services/proposals/kinds/task-close.js +76 -0
  112. package/dist/services/proposals/registry.js +4 -0
  113. package/dist/services/search-all.d.ts +5 -0
  114. package/dist/services/search-all.js +47 -14
  115. package/dist/services/task-stats.d.ts +41 -0
  116. package/dist/services/task-stats.js +126 -0
  117. package/dist/services/tasks.d.ts +17 -4
  118. package/dist/services/tasks.js +31 -14
  119. package/dist/store/chats.d.ts +3 -1
  120. package/dist/store/chats.js +6 -0
  121. package/dist/store/decisions.d.ts +4 -2
  122. package/dist/store/decisions.js +5 -3
  123. package/dist/store/exports.d.ts +1 -1
  124. package/dist/store/exports.js +1 -1
  125. package/dist/store/index.d.ts +2 -1
  126. package/dist/store/index.js +1 -1
  127. package/dist/store/sqlite/completeness.d.ts +17 -0
  128. package/dist/store/sqlite/completeness.js +49 -13
  129. package/dist/store/sqlite/lucene-fields.js +2 -0
  130. package/dist/store/sqlite/lucene-groups.d.ts +15 -0
  131. package/dist/store/sqlite/lucene-groups.js +21 -0
  132. package/dist/store/sqlite/lucene.d.ts +2 -9
  133. package/dist/store/sqlite/lucene.js +1 -12
  134. package/dist/store/sqlite/meeting-stats.d.ts +2 -0
  135. package/dist/store/sqlite/meeting-stats.js +27 -1
  136. package/dist/store/sqlite/retention.js +2 -7
  137. package/dist/store/sqlite/task-board.js +2 -0
  138. package/dist/store/sqlite/task-stats.d.ts +32 -0
  139. package/dist/store/sqlite/task-stats.js +45 -0
  140. package/dist/store/sqlite/tasks.d.ts +2 -1
  141. package/dist/store/sqlite/tasks.js +2 -0
  142. package/package.json +3 -3
  143. package/parity.json +13 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,131 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.565.0 — 05.11.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **The shared calendar commands print their own answers and open the profile per command.**
15
+ `addCalendarCommands(program, { app, open, freeBusy?, serverSearch?, feeds?, writes?, readFile?, writeFile? })`
16
+ replaces `CalendarCommandDeps`. `open(command, { readOnly })` returns a `CalendarRun` (event store, shared store,
17
+ account, profile, `locked`, and `source()`, `writes()`, `feeds()` built on first use); it is closed before the
18
+ answer prints. Each command answers in the standard envelope (`renderAnswer`) with its `meta` as built, so nothing
19
+ in between can drop it, and `write`, `note`, `embed`, `store`, `accountId` and the other per-profile fields are gone
20
+ from the options. The tree is built once with no state per call (WC-ARCH-22). `events export --output` and
21
+ `events import` read and write files themselves (0600, `file.not-found`, `file.not-a-file`, `file.denied`) unless
22
+ the app passes `readFile` / `writeFile`. `search all` is always mounted.
23
+ - **`calendars busy` answers and queries `starts_at` / `ends_at`**, as every event answer does, in place of `start`
24
+ / `end` (WC-OUT-12): `calendars busy starts_at>=2026-11-02 ends_at<=2026-11-06`.
25
+ - **`sync` keeps what the other calendars brought when one fails** (WC-OUT-8). It answers with `meta.complete: false`
26
+ and an issue per failed calendar (`sync.partial`, source `calendar:<id>`) instead of a `provider_error` with an
27
+ empty stdout.
28
+ - **`events create --text` takes `--operation-id`.** A quick add whose answer was lost names an id starting with
29
+ `q`; run again with it, the create looks for the event that attempt made through the writer's new optional
30
+ `quickAdded(calendar, since)` before it sends the line again (WC-CMD-6). Two candidates are refused
31
+ (`event.quick-add-ambiguous`).
32
+ - **`EntryContext.openBrowser(url, browser?, command?)` opens nothing where nobody can use a browser** (WC-CMD-15):
33
+ under `CI`, `--json`, `--jsonl`, `--no-input`, or with no terminal. The caller prints the URL either way.
34
+ - **Shared reasons for CLIs over a provider's API:** `offline.refused`, `api.unknown-method`, `api.disabled`,
35
+ `provider.usage-limit`, `oauth.scope-missing`, `sign-in.client-rejected`, `file.not-found`, `file.not-a-file` and
36
+ `file.denied` are in cli-core's list. A CLI that registered one of them itself must drop its copy: two entries of
37
+ one reason are refused.
38
+ - **`speech_model` and `picture_search` are messenger settings**, and `config show` of a CLI that is not a
39
+ messenger lists no messenger store setting (`phone_countries`, `task_categories.*`, `task_agent_close*`,
40
+ `task_first_pass_per_chat`).
41
+ - **cli-core accepts `@wirecat/cli-base` 0.76 and 0.77 (peer range `>=0.75.0 <0.78.0`) and builds with
42
+ 0.77.1**, whose vocabulary has the `schedule` resource, the `agent`, `command` and `exec` verbs and `--trimmed`. The command
43
+ lint now checks `schedule agent|command|exec` instead of skipping them in `lint/baseline`. A CLI that mounts
44
+ `schedule` (tg, max, wa, gcal, mail, memo) removes the same entries from its own baseline when it bumps cli-base
45
+ to 0.77.0: they are stale there and fail its lint. 0.76.0 adds the `conflict` error code; nothing here uses it yet.
46
+
47
+ ### Added
48
+
49
+ - **`actorOf(command)` is exported from `./cli`:** who acts in this run, the owner or the agent by its name, as a
50
+ `Decider` for the document knowledge's confirm, stale and reverse needs it.
51
+ - **`checkConfigText(text, { repair: false })` and `settingsFor(app, { configRepair: false })`** for a CLI with its
52
+ own `config` that has no `repair`: an invalid file's issue then names no `config repair` to run.
53
+ - **`commandPermissions({ alsoChecks })`:** the other keys a command's real run checks. A `--dry-run` preview shows
54
+ each with its level under `permission.also` and refuses as the run would, so gcal's `api.calendar.*` keys show.
55
+ - **A CLI that is not a messenger previews a `--dry-run` under `<PREFIX>_READ_ONLY`** (WC-PERM-12); it sends nothing.
56
+ A messenger's preview stays refused under the lock.
57
+
58
+ ## 0.564.0 — 05.11.2026
59
+
60
+ ### Changed — may break callers
61
+
62
+ - **`search all` ranks its sources as one list** (MEMO-37). Each source's hit at one rank used to get the same score,
63
+ so the sources took turns in the order they were read (messages, mail, notes, events). Now hits at the same rank
64
+ are ordered by cosine, since one model scores every source; a hit meaning did not find comes after those it did.
65
+ Each source's own order is kept. Without the model nothing changes. `search notes` and `search events` hits found
66
+ by meaning carry `meaning_score`, the cosine.
67
+ - **tg, max and wa `search all` change with the bump, with no code change in them.** Mail is searched by meaning when
68
+ the store holds mail, a question's message found both by words and by meaning gets one vote, and once vectors
69
+ exist the hits at one rank are ordered by meaning score.
70
+ - **`search all` searches mail by meaning too** (MEMO-38). Mail was only ever searched by words there, though its
71
+ threads had vectors. The meaning lane now reads a mail account's thread vectors through the mail store, and
72
+ `search all` fuses them with the word hits as it does for messages; the answer says how in `mail`. For a question,
73
+ a mail or message found by words gets no second vote from meaning, as in notes.
74
+ - **`search all` finds records for a question typed as a sentence.** When the text is a question in natural language
75
+ and not `exact`, messages and mail go through discovery (as `search messages --discover`), and notes and calendar
76
+ events search any of the question's content words, ranked by the store. Before, every word had to be in one record,
77
+ so a question found nothing (MEMO-36). tg, max and wa still ask the messenger's server with the question as typed.
78
+ For a question, a note found by both words and meaning gets one vote, not two, so notes found by words alone are not
79
+ buried. Plain words, query syntax and `exact` are searched strictly, as before. `searchNotes` and `searchEvents`
80
+ take `discover`.
81
+
82
+ ### Added
83
+
84
+ - **The proposal registry reviews meeting findings and task close suggestions.** `proposalRegistry` registers
85
+ `meeting_findings` (a meeting's staged batch, decided part by part as in `meetings actions confirm`) and
86
+ `task_close` (an agent's suggested close, id `close:<task key>`; confirming closes the task as suggested, rejecting
87
+ clears the suggestion). Both are owner only: `isOwnerOnlyKind` now names `task_close` too, so `mayDecide` refuses an
88
+ agent by the row's kind. `parseProposalIds` takes a task key after a prefix (`close:Q-15`). Moved from cli-memo,
89
+ which registered both itself; a host that registers a kind of the same name now gets `configuration_error`.
90
+
91
+ - **A mail CLI's `messages show` can read an email again when a sync's limit left its attachment unread.**
92
+ `mailReadCommands` takes `whole(command, store, mailbox, emailId)`; with it the command is a fetch, reads such an
93
+ email from the server before it answers (`readEmailWhole`), and shows the new text. When that read fails it shows
94
+ the stored email with `meta.complete: false` and an issue `mail.attachments-unread`. Without `whole` nothing changes.
95
+ `readEmailWhole`'s body and text limits, reader and clock now default as sync's do.
96
+ - **`has:transcript` in the messages query language** finds a voice message that has a transcription, so
97
+ `stats messages show --by day -- has:transcript date>=30d` counts transcribed voice messages per day. In mail it
98
+ finds nothing.
99
+ - **`store chats status`** counts the account's searchable chats of every kind, channels included, by how much of each
100
+ the archive holds: `{ chats, fetched, partial, never, oldest_message_at }`. `fetched` is held back to the first
101
+ message, up to date and without gaps; `never` has no fetch recorded and no message held; the rest is `partial`,
102
+ including WhatsApp chats whose history was pushed. One statement over the store, no list of chats. tg, max and wa
103
+ get it with the bump.
104
+ - **Meeting counts per day, week or series.** `readMeetingCounts` (services) and `store.meetings.meetingCounts`
105
+ answer meetings, minutes, mean minutes and meetings with a transcript, in total and per row, over the same
106
+ selection as `readMeetingStats`, reading `meetings` alone through `meetings_by_time`. `meetingCountsItem` and the
107
+ `meeting_counts` entity are the answer, for zoom-cli's `stats meetings history`. A meeting with no end counts in
108
+ `meetings`, not in `minutes`. `MeetingStatsStore` gains `meetingCounts`; only cli-core's own store implements it.
109
+ - **`stats tasks show` counts by chat, meeting or project, over a period.** `--by meeting` keys a row by the meeting a
110
+ task came from (`meeting:<account>/<meeting>`); `--by project` reads the owner's project tasks, every project
111
+ (`project:<id>`). A query after `--` takes the task query's fields and `created` for the period: `created` counts
112
+ the tasks opened in it, `closed` those closed in it, `open` is always open now (`-- created>=30d`). The item gains
113
+ `created`, `closed`, `median_close_ms` and `mean_open_age_ms`; each row gains `key`, `name` (chat title, meeting
114
+ title, project name), `created`, `closed` and `mean_open_age_ms`. Rows of `--by chat` keep `chat`; the others have
115
+ none. `--timezone` places calendar days such as `created>=today`.
116
+ - **`created:` in the project task query** (`PROJECT_TASK_FIELDS`), beside `closed:`: `created>=30d`.
117
+ - **`mailStats` from `./mail`**: stored mail received and sent per calendar day, week (by its Monday) or sender, for
118
+ the `search mail` query, counts only and no search recorded; `mailStatsItem` turns it into the new `mail_counts`
119
+ entity, `{ by, received, sent, rows: [{ key, name, received, sent }] }`. By sender it counts received mail only.
120
+ - **`mailOwnText(body)` (`@wirecat/cli-core/mail`): what an email says itself, without quoted earlier mail and the
121
+ signature.** It drops `>` lines, cuts at a reply header ("On … wrote:", "Le … a écrit :", "Am … schrieb …:",
122
+ "… написал(а):", Gmail's Russian "<address>:") and at an Outlook From/Sent block in English, German, French, Spanish
123
+ and Russian, and drops the `-- ` signature, sign-offs and mobile footers. A forwarded message stays whole; an email
124
+ that is only a quote keeps its body.
125
+
126
+ ### Security
127
+
128
+ - **`review --holder` gives each email's own text** (`mailOwnText`), so an agent's model does not read earlier mail
129
+ or signatures. The fingerprint a finding cites is still taken over the stored body; other reads show the whole text.
130
+ - **Review and inbox items say which fields others wrote** (WC-SECR-21): `summaryOf` items, and so every catch-up
131
+ answer and each mail `review` item, carry `untrusted` (`SUMMARY_UNTRUSTED`: chat title, sender name, text,
132
+ transcript, attachment names); a mail review item adds the email's subject and the names in its headers, as search
133
+ hits do. The `message.summary` entity takes the field.
134
+
10
135
  ## 0.563.0 — 03.11.2026
11
136
 
12
137
  ### Changed — may break callers
@@ -8,7 +8,7 @@ export interface BusyQuery {
8
8
  people: string[];
9
9
  }
10
10
  /**
11
- * `calendars busy`'s query: the window by the answer's own `start` and `end`, and whom to ask. `calendar` and
11
+ * `calendars busy`'s query: the window by the answer's own `starts_at` and `ends_at`, and whom to ask. `calendar` and
12
12
  * `person` repeat, and every one named is asked: the free stretches are those they all share.
13
13
  */
14
14
  export declare const BUSY_QUERY: QueryRegistry;
@@ -47,29 +47,29 @@ export declare const busyAnswer: (source: CalendarSource, query: BusyQuery, opti
47
47
  hours: WorkingHours;
48
48
  };
49
49
  }) => Promise<{
50
- start: string;
51
- end: string;
50
+ starts_at: string;
51
+ ends_at: string;
52
52
  timezone: string;
53
53
  calendars: ({
54
54
  id: string;
55
55
  calendar: string;
56
56
  busy: {
57
- start: string;
58
- end: string;
57
+ starts_at: string;
58
+ ends_at: string;
59
59
  }[];
60
60
  error?: string | undefined;
61
61
  } | {
62
62
  id: string;
63
63
  person: string;
64
64
  busy: {
65
- start: string;
66
- end: string;
65
+ starts_at: string;
66
+ ends_at: string;
67
67
  }[];
68
68
  error?: string | undefined;
69
69
  })[];
70
70
  slots?: {
71
- start: string;
72
- end: string;
71
+ starts_at: string;
72
+ ends_at: string;
73
73
  }[] | undefined;
74
74
  unknown?: string[] | undefined;
75
75
  meta: {
@@ -78,7 +78,7 @@ export declare const busyAnswer: (source: CalendarSource, query: BusyQuery, opti
78
78
  timezone: string;
79
79
  complete: boolean;
80
80
  issues?: {
81
- code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
81
+ code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
82
82
  reason?: string | undefined;
83
83
  source: string;
84
84
  }[] | undefined;
@@ -19,14 +19,14 @@ const bound = (field, side, example) => (node) => {
19
19
  queryError("query.unsupported-operator", node.span, `${field} takes only ${side === "lower" ? "the window's start" : "the window's end"}, such as ${example}`, { command: BUSY, guide: LIST_QUERY_GUIDE });
20
20
  };
21
21
  /**
22
- * `calendars busy`'s query: the window by the answer's own `start` and `end`, and whom to ask. `calendar` and
22
+ * `calendars busy`'s query: the window by the answer's own `starts_at` and `ends_at`, and whom to ask. `calendar` and
23
23
  * `person` repeat, and every one named is asked: the free stretches are those they all share.
24
24
  */
25
25
  export const BUSY_QUERY = {
26
26
  entity: "calendar_busy",
27
27
  backend: "native",
28
28
  fields: queryFieldsOf(calendarBusy, {
29
- native: ["start", "end", "calendar", "person"],
29
+ native: ["starts_at", "ends_at", "calendar", "person"],
30
30
  relations: {
31
31
  calendar: {
32
32
  type: "kw",
@@ -35,16 +35,16 @@ export const BUSY_QUERY = {
35
35
  },
36
36
  person: { type: "kw", example: "person:ana@example.com", relation: "someone else, by address; repeat for more" },
37
37
  },
38
- examples: { start: "start>=2026-11-02", end: "end<=2026-11-06" },
38
+ examples: { starts_at: "starts_at>=2026-11-02", ends_at: "ends_at<=2026-11-06" },
39
39
  validate: {
40
- start: bound("start", "lower", "start>=2026-11-02"),
41
- end: bound("end", "upper", "end<=2026-11-06"),
40
+ starts_at: bound("starts_at", "lower", "starts_at>=2026-11-02"),
41
+ ends_at: bound("ends_at", "upper", "ends_at<=2026-11-06"),
42
42
  },
43
43
  }).map((field) => field.native
44
44
  ? { ...field, operators: field.type === "time" ? ["range"] : ["term", "phrase"] }
45
45
  : field),
46
46
  };
47
- export const BUSY_QUERY_NOTE = "\nWithout start: from the start of today, in your time zone. Without end: seven days after the start. " +
47
+ export const BUSY_QUERY_NOTE = "\nWithout starts_at: from the start of today, in your time zone. Without ends_at: seven days after the start. " +
48
48
  "Without calendar: every calendar that syncs.";
49
49
  export const busyQuery = async (query, context) => {
50
50
  const { zone, now } = context;
@@ -56,9 +56,9 @@ export const busyQuery = async (query, context) => {
56
56
  const terms = nativeTermLists(listQuery(typedQuery(query), BUSY_QUERY, BUSY), BUSY, ["calendar", "person"]);
57
57
  for (const [field, nodes] of terms)
58
58
  for (const node of nodes) {
59
- if (field === "start")
59
+ if (field === "starts_at")
60
60
  from = timeWindow(node, zone, now).lower;
61
- else if (field === "end")
61
+ else if (field === "ends_at")
62
62
  to = timeWindow(node, zone, now).upper;
63
63
  else if (field === "calendar") {
64
64
  const calendar = await calendarIn(context.store, context.accountId, node.value);
@@ -135,7 +135,7 @@ export const freeSlots = (busy, window, options) => {
135
135
  return slots;
136
136
  };
137
137
  const iso = (at) => new Date(at).toISOString();
138
- const times = ({ from, to }) => ({ start: iso(from), end: iso(to) });
138
+ const times = ({ from, to }) => ({ starts_at: iso(from), ends_at: iso(to) });
139
139
  /**
140
140
  * `calendars busy`: the provider's busy times for the query, and with `slot` the free stretches every calendar and
141
141
  * person shares. Nothing is stored, and a person appears only by the address the caller gave.
@@ -38,8 +38,8 @@ export declare const calendarFeedRemoval: v.StrictObjectSchema<{
38
38
  readonly events: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
39
39
  }, undefined>;
40
40
  export declare const calendarBusy: v.StrictObjectSchema<{
41
- readonly start: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
42
- readonly end: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
41
+ readonly starts_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
42
+ readonly ends_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
43
43
  readonly timezone: v.StringSchema<undefined>;
44
44
  readonly calendars: v.ArraySchema<v.StrictObjectSchema<{
45
45
  /** One of the account's calendars, by its id; or `person`, by the address asked. */
@@ -47,14 +47,14 @@ export declare const calendarBusy: v.StrictObjectSchema<{
47
47
  readonly person: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
48
48
  readonly id: v.StringSchema<undefined>;
49
49
  readonly busy: v.ArraySchema<v.StrictObjectSchema<{
50
- readonly start: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
51
- readonly end: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
50
+ readonly starts_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
51
+ readonly ends_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
52
52
  }, undefined>, undefined>;
53
53
  readonly error: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
54
54
  }, undefined>, undefined>;
55
55
  readonly slots: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
56
- readonly start: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
57
- readonly end: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
56
+ readonly starts_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
57
+ readonly ends_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
58
58
  }, undefined>, undefined>, undefined>;
59
59
  readonly unknown: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
60
60
  }, undefined>;
@@ -505,8 +505,7 @@ export declare const calendarLinkResolution: v.StrictObjectSchema<{
505
505
  readonly chosen: v.NullableSchema<v.StringSchema<undefined>, undefined>;
506
506
  }, undefined>;
507
507
  /**
508
- * Each command's answer entity: one item of a list, or the one value. They describe what a host prints, with the
509
- * keys in snake_case, though a command hands its host some keys in camelCase (`notSynced`, `hasMore`).
508
+ * Each command's answer entity: one item of a list, or the one value, as the command prints it.
510
509
  */
511
510
  export declare const declareCalendarAnswers: (program: Command, commands: readonly Command[]) => void;
512
511
  export declare const CALENDAR_ANSWER_PATHS: string[];
@@ -7,7 +7,7 @@ import { count, id, reference, text, time, untrusted } from "../../entities/fiel
7
7
  import { event } from "../../meetings/index.js";
8
8
  const nullableText = v.nullable(text);
9
9
  const nullableTime = v.nullable(time);
10
- const stretch = v.strictObject({ start: time, end: time });
10
+ const stretch = v.strictObject({ starts_at: time, ends_at: time });
11
11
  const epochWindow = v.strictObject({ from: v.number(), to: v.number() });
12
12
  export const calendar = v.strictObject({
13
13
  id,
@@ -32,8 +32,8 @@ const busyRow = v.strictObject({
32
32
  error: v.optional(text),
33
33
  });
34
34
  export const calendarBusy = v.strictObject({
35
- start: time,
36
- end: time,
35
+ starts_at: time,
36
+ ends_at: time,
37
37
  timezone: text,
38
38
  calendars: v.array(busyRow),
39
39
  slots: v.optional(v.array(stretch)),
@@ -274,8 +274,7 @@ const ANSWERS = {
274
274
  "links resolve": { name: "calendar_link_resolution", schema: calendarLinkResolution },
275
275
  };
276
276
  /**
277
- * Each command's answer entity: one item of a list, or the one value. They describe what a host prints, with the
278
- * keys in snake_case, though a command hands its host some keys in camelCase (`notSynced`, `hasMore`).
277
+ * Each command's answer entity: one item of a list, or the one value, as the command prints it.
279
278
  */
280
279
  export const declareCalendarAnswers = (program, commands) => {
281
280
  const pathOf = (command) => {
@@ -0,0 +1,33 @@
1
+ import type { NextStep } from "@wirecat/cli-base/standards";
2
+ import type { Command } from "commander";
3
+ import { type Body } from "../../cli/envelope.js";
4
+ import type { MessageStore } from "../../store/store.js";
5
+ import type { CalendarRun } from "./index.js";
6
+ /** The command's words below the program, as `page.next` and `sync`'s next step rerun it. */
7
+ export declare const pathOf: (command: Command) => string[];
8
+ interface Listed {
9
+ items: object[];
10
+ page?: number;
11
+ limit?: number;
12
+ hasMore?: boolean;
13
+ }
14
+ /** A list the shared reads answer (`listed`), as the envelope: `page.next` reruns the same query one page on. */
15
+ export declare const listBody: (command: Command, { items, page, limit, hasMore }: Listed, meta?: object) => Body;
16
+ export declare const itemBody: (value: object, extra?: {
17
+ meta?: object;
18
+ next?: NextStep[];
19
+ }) => Body;
20
+ interface Hits {
21
+ hits: object[];
22
+ has_more?: boolean;
23
+ next_offset?: number;
24
+ meaning_skipped?: string;
25
+ untrusted?: string[];
26
+ }
27
+ /**
28
+ * A search's hits as a list with cursor paging (WC-OUT-4, WC-OUT-5); each hit names its own untrusted fields, as
29
+ * `events list` does. Without a cursor there is no next page to run: the provider's search pages per calendar.
30
+ */
31
+ export declare const hitsBody: (command: Command, value: Hits) => Body;
32
+ export declare const messagesOf: (run: CalendarRun, what: string) => MessageStore;
33
+ export {};
@@ -0,0 +1,52 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { itemOf, listOf, snakeKeys } from "../../cli/envelope.js";
3
+ /** The command's words below the program, as `page.next` and `sync`'s next step rerun it. */
4
+ export const pathOf = (command) => command.parent?.parent ? [...pathOf(command.parent), command.name()] : [command.name()];
5
+ const snakeMeta = (meta) => meta === undefined ? undefined : snakeKeys(meta);
6
+ /** A list the shared reads answer (`listed`), as the envelope: `page.next` reruns the same query one page on. */
7
+ export const listBody = (command, { items, page, limit, hasMore }, meta) => {
8
+ const query = command.processedArgs[0];
9
+ const facts = snakeMeta(meta);
10
+ return listOf(pathOf(command).join(" "), {
11
+ items: items.map((item) => snakeKeys(item)),
12
+ limit: Math.max(1, limit ?? items.length),
13
+ has_more: hasMore === true,
14
+ ...(page === undefined ? {} : { page }),
15
+ ...(Array.isArray(query) && query.length > 0 ? { args: { query } } : {}),
16
+ ...(facts ? { meta: facts } : {}),
17
+ });
18
+ };
19
+ export const itemBody = (value, extra = {}) => {
20
+ const meta = snakeMeta(extra.meta);
21
+ return itemOf(snakeKeys(value), { ...(meta ? { meta } : {}), ...(extra.next ? { next: extra.next } : {}) });
22
+ };
23
+ /**
24
+ * A search's hits as a list with cursor paging (WC-OUT-4, WC-OUT-5); each hit names its own untrusted fields, as
25
+ * `events list` does. Without a cursor there is no next page to run: the provider's search pages per calendar.
26
+ */
27
+ export const hitsBody = (command, value) => {
28
+ const opts = command.opts();
29
+ const untrusted = (value.untrusted ?? []).filter((field) => field.startsWith("hits.")).map((f) => f.slice(5));
30
+ const { next, ...list } = listOf(pathOf(command).join(" "), {
31
+ items: value.hits.map((hit) => snakeKeys({ ...hit, untrusted })),
32
+ limit: opts.limit,
33
+ has_more: value.has_more === true,
34
+ paging: "cursor",
35
+ ...(typeof value.next_offset === "number" ? { cursor: String(value.next_offset) } : {}),
36
+ args: {
37
+ query: command.processedArgs[0],
38
+ ...(opts.exact ? { exact: true } : {}),
39
+ ...(opts.newest ? { newest: true } : {}),
40
+ },
41
+ ...(typeof value.meaning_skipped === "string"
42
+ ? { missing: [{ field: "items", why: `not searched by meaning: ${value.meaning_skipped}` }] }
43
+ : {}),
44
+ });
45
+ const kept = typeof value.next_offset === "number" ? next : next?.filter((one) => one.step !== "page.next");
46
+ return { ...list, ...(kept?.length ? { next: kept } : {}) };
47
+ };
48
+ export const messagesOf = (run, what) => {
49
+ if (!run.messages)
50
+ throw new CliError("configuration_error", `${what} needs the shared store`);
51
+ return run.messages;
52
+ };
@@ -0,0 +1,8 @@
1
+ /** The largest .ics file `events import` reads; checked before the file is read whole. */
2
+ export declare const ICS_MAX_BYTES: number;
3
+ export declare const readIcsFile: (path: string) => Promise<string>;
4
+ /**
5
+ * 0600 even over an existing file, written whole or not at all: an export holds the owner's meetings, attendees and
6
+ * descriptions. writeSecurely would make a missing folder; an export names one that exists.
7
+ */
8
+ export declare const writeIcsFile: (path: string, content: string) => Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { CliError, writeSecurely } from "@wirecat/cli-base";
4
+ /** The largest .ics file `events import` reads; checked before the file is read whole. */
5
+ export const ICS_MAX_BYTES = 10 * 1024 * 1024;
6
+ const FILE_ERRORS = {
7
+ ENOENT: ["not_found", "file.not-found", "does not exist"],
8
+ ENOTDIR: ["not_found", "file.not-found", "is not in a folder that exists"],
9
+ EISDIR: ["validation_error", "file.not-a-file", "is a folder, not a file"],
10
+ EACCES: ["permission_error", "file.denied", "cannot be opened: permission denied"],
11
+ EPERM: ["permission_error", "file.denied", "cannot be opened: permission denied"],
12
+ };
13
+ /** A file the owner named that cannot be read or written is their error, named by its path, never a crash. */
14
+ const named = async (path, act) => {
15
+ try {
16
+ return await act();
17
+ }
18
+ catch (error) {
19
+ const known = FILE_ERRORS[error.code ?? ""];
20
+ if (!known)
21
+ throw error;
22
+ const [code, reason, words] = known;
23
+ throw new CliError(code, `${path} ${words}`, { reason });
24
+ }
25
+ };
26
+ export const readIcsFile = (path) => named(path, async () => {
27
+ const info = await stat(path);
28
+ if (!info.isFile())
29
+ throw new CliError("validation_error", `${path} is not a file`, { reason: "file.not-a-file" });
30
+ if (info.size > ICS_MAX_BYTES)
31
+ throw new CliError("validation_error", `${path} is larger than 10 MB`, { reason: "ics.too-large" });
32
+ return readFile(path, "utf8");
33
+ });
34
+ /**
35
+ * 0600 even over an existing file, written whole or not at all: an export holds the owner's meetings, attendees and
36
+ * descriptions. writeSecurely would make a missing folder; an export names one that exists.
37
+ */
38
+ export const writeIcsFile = (path, content) => named(path, async () => {
39
+ await stat(dirname(path));
40
+ writeSecurely(path, content, 0o600);
41
+ });
@@ -1,70 +1,77 @@
1
1
  import { type Command } from "commander";
2
+ import { type Body } from "../../cli/envelope.js";
2
3
  import type { EventStore } from "../../meetings/index.js";
3
4
  import type { AppIdentity } from "../../ports/app.js";
4
5
  import type { AccountKey, MessageStore } from "../../store/store.js";
5
6
  import { type CalendarFeeds } from "../feeds.js";
6
7
  import type { CalendarExpander, CalendarSource } from "../source.js";
7
8
  import { type CalendarWrites } from "../writes.js";
8
- export interface CalendarCommandDeps {
9
- store: EventStore;
10
- /** The selected profile's calendar account: every command reads and syncs only it. */
11
- accountId: number;
12
- /** The same account as the shared store names it, for search and people. */
9
+ /**
10
+ * The selected profile's calendar, opened by the app for one command and closed once its answer is built, before
11
+ * anything is printed (WC-LIFE-4). Nothing of it outlives the command, so concurrent runs share no state (WC-ARCH-22).
12
+ */
13
+ export interface CalendarRun {
14
+ /** The profile's name, which `store status` gives as each sync's `profile`. */
15
+ profile: string;
16
+ /** The profile's calendar account as the shared store names it: every command reads and syncs only it. */
13
17
  account: AccountKey;
14
- /** The profile's name, which `status` gives as each sync's `profile`. */
15
- profile?: string;
16
- /** The shared store: `search`, `people link` and the person of an attendee read it. */
18
+ accountId: number;
19
+ store: EventStore;
20
+ /** The shared store: `search`, `people link`, `--source` and an attendee's person read it. */
17
21
  messages?: MessageStore;
18
- /** The provider; `sync` and `calendars list` read it. */
19
- source?: CalendarSource;
20
- /**
21
- * The source answers `freeBusy`: mounts `calendars busy`. A flag rather than a look at `source`, which a CLI may
22
- * build only once a profile is open.
23
- */
24
- freeBusy?: boolean;
25
- /**
26
- * The source answers `search`: `search` takes `--backend server` and asks the provider. A flag, as `freeBusy`, so the
27
- * source can stay unbuilt until a profile is open.
28
- */
29
- serverSearch?: boolean;
22
+ /** The provider, built on first use: a command that only reads the store never reads the sign-in. */
23
+ source: () => CalendarSource;
24
+ /** The provider's writer and the profile's guard, for the write commands. */
25
+ writes?: () => CalendarWrites;
26
+ /** The iCalendar feeds the profile follows; `sync` and `calendars list` read them after the provider. */
27
+ feeds?: () => CalendarFeeds;
30
28
  /** For a source with `expands: "local"`. */
31
29
  expand?: CalendarExpander;
30
+ env?: NodeJS.ProcessEnv;
32
31
  /**
33
- * The iCalendar feeds the profile follows, as read-only calendars of its account: mounts `calendars add|remove`,
34
- * and `sync` and `calendars list` read them after the provider. Read only once a profile is open.
35
- */
36
- feeds?: CalendarFeeds;
37
- /** After `sync --embed`: embeds new events for search by meaning (`embedEvents`), as drive's `sync --embed`. */
38
- embed?: (max: number) => Promise<{
39
- model: string;
40
- embedded: number;
41
- left: boolean;
42
- }>;
43
- /**
44
- * `view` is the text a person reads in place of the default rendering: the times on the zone's wall clock, and the
45
- * zone named. It takes the host's own body (`renderAnswer` passes it) or, without one, renders `value`.
32
+ * A profile lock pins the process (WC-ACCT-5): `events list --source` may then read only the profile's account, and
33
+ * `links review` lists only its choices.
46
34
  */
47
- write: (value: unknown, format: "json" | "jsonl" | "text", view?: (body?: unknown) => string) => void;
48
- /** A line for the person beside the answer, never in it: which accounts `search all` read and which it skipped. */
49
- note?: (text: string) => void;
35
+ locked?: boolean;
36
+ close?: () => Promise<void>;
37
+ }
38
+ /** What the provider can do, known when the commands are built, before any profile is open. */
39
+ export interface CalendarCommandOptions {
40
+ /** The CLI itself: the first word of examples, the store's command name, and `search all`. */
41
+ app: AppIdentity;
50
42
  /**
51
- * The CLI itself. With it and `messages`, `search` also has `all`: messages, notes, events and named meetings in the
52
- * shared store, as a messenger's `search all`.
43
+ * Opens the selected profile's calendar for one command. `readOnly` is true when the command only reads the store, so
44
+ * parallel reads take no write lock; the app may still open it writable, for a profile's first run say.
53
45
  */
54
- app?: AppIdentity;
46
+ open: (command: Command, access: {
47
+ readOnly: boolean;
48
+ }) => Promise<CalendarRun>;
49
+ /** The source answers `freeBusy`: mounts `calendars busy`. */
50
+ freeBusy?: boolean;
51
+ /** The source answers `search`: `search events` takes `--backend server|both`. */
52
+ serverSearch?: boolean;
53
+ /** The profile may follow iCalendar feeds: mounts `calendars add|remove`. */
54
+ feeds?: boolean;
55
+ /** The provider takes writes: mounts `events create|update|delete|respond|remind`, and what its writer adds. */
56
+ writes?: {
57
+ quickAdd?: boolean;
58
+ move?: boolean;
59
+ importEvent?: boolean;
60
+ };
61
+ /** Reads `events import`'s file; a file on disk, at most 10 MB, when absent. */
62
+ readFile?: (path: string) => Promise<string>;
63
+ /** Writes `events export --output`'s file; owner-only (0600), whole or not at all, when absent. */
64
+ writeFile?: (path: string, content: string) => Promise<void>;
55
65
  now?: () => number;
56
66
  /** The zone `today`, `week` and a bare date are read in; the computer's when absent. */
57
67
  timezone?: string;
58
- /** The provider's writer and the profile's guard: `events create|update|delete|respond` exist only with it. */
59
- writes?: CalendarWrites;
60
- /** Writes `events export --output`'s file; without it the export is printed only. */
61
- writeFile?: (path: string, content: string) => Promise<void>;
62
- /** Reads `events import`'s file; without it, and the writer's `import`, there is no `events import`. */
63
- readFile?: (path: string) => Promise<string>;
64
- /**
65
- * A profile lock pins the process (WC-ACCT-5): `events list --source` may then read only the profile's account, and
66
- * `links review` lists only its choices.
67
- */
68
- locked?: boolean;
69
68
  }
70
- export declare const addCalendarCommands: (program: Command, deps: CalendarCommandDeps) => void;
69
+ /** Opens the profile for one command, builds its answer and prints it once the profile is closed. */
70
+ export type Within = (command: Command, work: (run: CalendarRun) => Promise<Body>, view?: (body: Body) => string) => Promise<void>;
71
+ export interface CommandHelpers {
72
+ within: Within;
73
+ now: () => number;
74
+ zone: () => string;
75
+ example: (command: Command, ...lines: string[]) => Command;
76
+ }
77
+ export declare const addCalendarCommands: (program: Command, options: CalendarCommandOptions) => void;