@wirecat/cli-core 0.419.0 → 0.421.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 (126) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli/bot/context.d.ts +3 -2
  3. package/dist/cli/bot/context.js +15 -0
  4. package/dist/cli/config-command.js +1 -2
  5. package/dist/cli/index.d.ts +0 -1
  6. package/dist/cli/index.js +0 -1
  7. package/dist/cli/messenger/context.js +3 -6
  8. package/dist/cli/permission-keys.d.ts +2 -0
  9. package/dist/cli/permission-keys.js +10 -3
  10. package/dist/cli/preview.d.ts +2 -2
  11. package/dist/cli/preview.js +4 -3
  12. package/dist/cli/program.js +4 -8
  13. package/dist/cli/settings.d.ts +0 -2
  14. package/dist/cli/settings.js +1 -5
  15. package/dist/domain/providers.d.ts +20 -2
  16. package/dist/domain/providers.js +23 -3
  17. package/dist/domain/tag-consolidation.d.ts +30 -1
  18. package/dist/entities/index.d.ts +4 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/mcp/surface.js +0 -4
  22. package/dist/mcp/tool.d.ts +1 -3
  23. package/dist/mcp/tool.js +1 -2
  24. package/dist/meetings/calendar-text.d.ts +13 -0
  25. package/dist/meetings/calendar-text.js +57 -0
  26. package/dist/meetings/event-store.d.ts +202 -0
  27. package/dist/meetings/event-store.js +1 -0
  28. package/dist/meetings/events.js +5 -1
  29. package/dist/meetings/index.d.ts +3 -0
  30. package/dist/meetings/index.js +2 -0
  31. package/dist/meetings/join-key.d.ts +7 -0
  32. package/dist/meetings/join-key.js +38 -0
  33. package/dist/meetings/mcp/index.d.ts +15 -15
  34. package/dist/meetings/model.d.ts +8 -2
  35. package/dist/meetings/read-answers.d.ts +6 -2
  36. package/dist/meetings/read-answers.js +9 -2
  37. package/dist/meetings/store.d.ts +2 -0
  38. package/dist/meetings/testing/event-store-contract.d.ts +10 -0
  39. package/dist/meetings/testing/event-store-contract.js +161 -0
  40. package/dist/meetings/testing/index.d.ts +1 -0
  41. package/dist/meetings/testing/index.js +1 -0
  42. package/dist/parity/manifest.js +0 -4
  43. package/dist/search/account-list.js +39 -7
  44. package/dist/sends/index.d.ts +1 -1
  45. package/dist/sends/index.js +1 -1
  46. package/dist/sends/permissions.d.ts +0 -13
  47. package/dist/sends/permissions.js +15 -142
  48. package/dist/services/agenda.js +5 -5
  49. package/dist/services/document-source.d.ts +7 -3
  50. package/dist/services/document-source.js +24 -12
  51. package/dist/services/documents/ingestion.d.ts +6 -1
  52. package/dist/services/documents/ingestion.js +4 -0
  53. package/dist/services/notes-search.js +3 -2
  54. package/dist/services/search-all.js +1 -0
  55. package/dist/services/task-tag-filters.d.ts +3 -0
  56. package/dist/services/task-tag-filters.js +8 -0
  57. package/dist/services/tasks.js +5 -9
  58. package/dist/store/document-transfer/index.js +12 -2
  59. package/dist/store/index.d.ts +2 -1
  60. package/dist/store/sqlite/accounts.js +1 -0
  61. package/dist/store/sqlite/calendar.d.ts +3 -0
  62. package/dist/store/sqlite/calendar.js +593 -0
  63. package/dist/store/sqlite/catalog.js +16 -5
  64. package/dist/store/sqlite/chat-rekey.js +1 -1
  65. package/dist/store/sqlite/chats.js +2 -2
  66. package/dist/store/sqlite/context-document-source.js +8 -4
  67. package/dist/store/sqlite/document-transfer/apply.js +24 -2
  68. package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
  69. package/dist/store/sqlite/document-transfer/collection.js +66 -0
  70. package/dist/store/sqlite/document-transfer/plan.js +15 -8
  71. package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
  72. package/dist/store/sqlite/entity-types.d.ts +5 -1
  73. package/dist/store/sqlite/entity-types.js +5 -1
  74. package/dist/store/sqlite/events.d.ts +8 -2
  75. package/dist/store/sqlite/events.js +36 -12
  76. package/dist/store/sqlite/files.d.ts +93 -0
  77. package/dist/store/sqlite/files.js +293 -0
  78. package/dist/store/sqlite/folded.d.ts +3 -0
  79. package/dist/store/sqlite/folded.js +1 -0
  80. package/dist/store/sqlite/imported-documents.js +1 -1
  81. package/dist/store/sqlite/join-keys.d.ts +8 -0
  82. package/dist/store/sqlite/join-keys.js +27 -0
  83. package/dist/store/sqlite/knowledge.js +6 -6
  84. package/dist/store/sqlite/link-reviews.d.ts +9 -0
  85. package/dist/store/sqlite/link-reviews.js +12 -0
  86. package/dist/store/sqlite/lucene-fields.js +8 -6
  87. package/dist/store/sqlite/manifest.js +11 -0
  88. package/dist/store/sqlite/meeting-selection.js +1 -1
  89. package/dist/store/sqlite/meetings.js +6 -1
  90. package/dist/store/sqlite/migrations.generated.js +143 -0
  91. package/dist/store/sqlite/note-index.d.ts +8 -1
  92. package/dist/store/sqlite/note-index.js +14 -0
  93. package/dist/store/sqlite/note-search.js +6 -5
  94. package/dist/store/sqlite/notes.d.ts +18 -1
  95. package/dist/store/sqlite/notes.js +237 -40
  96. package/dist/store/sqlite/open-store.js +4 -0
  97. package/dist/store/sqlite/person-links.js +7 -2
  98. package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
  99. package/dist/store/sqlite/schema/canonical-tags.js +8 -0
  100. package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
  101. package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
  102. package/dist/store/sqlite/schema.d.ts +3589 -1634
  103. package/dist/store/sqlite/schema.js +191 -13
  104. package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
  105. package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
  106. package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
  107. package/dist/store/sqlite/tag-consolidation-input.js +52 -0
  108. package/dist/store/sqlite/tag-consolidation.js +51 -66
  109. package/dist/store/sqlite/tag-creation.js +17 -8
  110. package/dist/store/sqlite/tag-resolution.d.ts +5 -0
  111. package/dist/store/sqlite/tag-resolution.js +33 -0
  112. package/dist/store/sqlite/tags.d.ts +3 -1
  113. package/dist/store/sqlite/tags.js +70 -50
  114. package/dist/store/sqlite/tasks.d.ts +2 -0
  115. package/dist/store/sqlite/tasks.js +6 -1
  116. package/dist/store/sqlite/things.js +2 -1
  117. package/dist/store/sqlite/vocabulary.d.ts +1 -0
  118. package/dist/store/sqlite/vocabulary.js +30 -8
  119. package/dist/store/store.d.ts +6 -0
  120. package/dist/store/store.js +10 -0
  121. package/dist/store/tags.js +2 -2
  122. package/package.json +2 -1
  123. package/dist/cli/moved.d.ts +0 -18
  124. package/dist/cli/moved.js +0 -72
  125. package/dist/sends/permission-aliases.d.ts +0 -5
  126. package/dist/sends/permission-aliases.js +0 -50
package/CHANGELOG.md CHANGED
@@ -7,6 +7,54 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.421.0 — 25.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **Old command names and old permission keys are no longer recognised.** A retired command path (`contacts profile`, `bot store fetch`, `search mail` and the rest) gets the ordinary "unknown command" answer, on the command line and over MCP, with no pointer to a replacement. A saved permission key is read only as written: nothing maps an old key to a new one, and `chats.mute` no longer covers `chats unmute`. Removed with it: `movedCommandError` from `/cli`; `PERMISSIONS`, `Permission`, `permissionFor` and `assertStatsPermissionsCurrent` from `/sends`; the `permission` field of an MCP tool definition; and the `skipMovedPermissions` option of `resolveSettings`. A CLI that imports one of them drops the import; nothing replaces them.
15
+ - **Bot commands and `--dry-run` refuse a saved permission key that no command checks**, as messenger commands already did. They stop with a `configuration_error` that names the key and the `config unset` that removes it: "permissions.bot.store.fetch names no command — remove it with \`tg sales config unset --bot permissions.bot.store.fetch\`". `config show`, `set` and `unset` still run, so the file can be repaired. Before, a bot command did not look at the keys at all, and an old key was quietly renamed or ignored.
16
+ - **`messages sync` and `bot messages sync` are checked as `messages.sync` and `bot.messages.sync`**, their own paths. A level on `messages` or `bot.messages` still applies to them; `commands --json` now reports the longer key.
17
+
18
+ ### Fixed
19
+
20
+ - **A deny saved on the sync command itself now stops it.** `permissions.bot.messages.sync: deny` was ignored, because `bot messages sync` was checked under `bot.messages`; `permissions.messages.sync: deny` could not be saved at all and was refused as naming no command. Both are now honoured on the command line, in MCP discovery and by the MCP server. Bot commands also refuse a denied read (`bot.messages: deny` stops `bot messages list` and `bot messages sync`) before they connect, as messenger commands already did.
21
+
22
+ ## 0.420.0 — 25.10.2026
23
+
24
+ ### Changed — may break callers
25
+
26
+ - **A meeting links to an event by its room, with exclusive ends, and two candidate events make no link** (`eventCandidates`, `linkEvent`; available from the cli-core release that carries store versions 30 to 33). Rooms are compared by `join_key`, never by the raw link with its passcode; an event a calendar entry holds is a candidate though it has no meeting yet; back-to-back events no longer both match. With two or more candidates `linkEvent` links none and creates none, and `MeetingStore.reviewMeetingLink` (optional; the store's own implements it) puts the meeting on the owner's review list. Before, it created a new automatic event: a second event for a meeting the calendar already held.
27
+ - **`Event` gains optional `status`, `allDay` and `sourceDeletedAt`, and `origin` on `Event` and `EventSeries` may be `calendar`** (store version 33; available from the cli-core release that carries store versions 30 to 33). The stored event answer (`events list`, its MCP tool; a strict schema) gains `status`, `all_day` and `source_deleted_at`, and its `origin` takes `calendar`. A caller that switches over `origin` or validates the answer against the old schema must take the new value and fields.
28
+ - **Notes import finds a file by its place in `files`, and a new file with one document's content is that document** (requirements 3.3, rule 2; available from the cli-core release that carries store versions 30 to 33). `saveFileNote`, `renameFileNote`, `deleteFileNotes` and `notes({ folderId })` work through each folder's `files` rows, so one document can live in several folders, each listing it under its own path. A new local file whose content hash equals one document's is a copy of it — a live one, else a deleted one no file holds any more, so a file moved between folders keeps its document in whichever order the folders sync; two such documents give a new document and one review entry; empty text never joins. A document stays live while any of its files is, and its own `account_id` and `external_id` follow the file it shows (the owner's pin, else the latest changed); Drive's documents keep theirs. A file renamed and edited in one sync is a new document, and the old one stays, deleted. `saveFileNote` and `renameFileNote` answer the note as the saving folder sees it: its `folderId` and `path` are that file's.
29
+ - **Reviewed tag consolidation (store version 30)** (available from the cli-core release that carries store versions 30 to 33). Tag and automatic-claim
30
+ tables move behind a structural compatibility boundary with `minCompatible: 30`: `tags` becomes `canonical_tags`
31
+ and `auto_tag_claims` becomes `canonical_auto_tag_claims`, so an older SDK refuses the migrated file, including a
32
+ handle it already holds open. Later migrations must keep that minimum; the manifest refuses one that omits or
33
+ lowers it. Version 30 ships in one breaking release with versions 31 to 33. Consolidated tag groups refuse
34
+ document annotation transfer before copying; existing destinations need current schema maintenance for a
35
+ read-only preview.
36
+ - **Drive documents move from their stand-in notes folder to the `google-drive` account (store version 31, `minCompatible` 31)** (available from the cli-core release that carries store versions 30 to 33). The store moves every Drive file of a `folder` named `google-drive:<principal>` to the `{ provider: "google-drive", account: <principal> }` account by `UPDATE`, so `document:<id>` references, chunks, tags, notes and links keep pointing where they did; the folder row goes, saved profiles `{ provider: "folder", account: "google-drive:<p>" }` become `{ provider: "google-drive", account: "<p>" }`, and group entries `notes:<folder id>` follow to the account's id. A CLI on an older cli-core refuses the store until it is upgraded. `notes.folders()` no longer lists Drive; `notes.documentsAccount(key, create)` and `notes.documentsAccounts()` find it, and `documentIngestion({ provider: "google-drive" })` writes into it (`collectionName` is now optional). `folder:<id>`, `--source drive:<profile>`, `--source google-drive:<principal>` and annotation transfers (`google-drive:<principal>`, from a store on either side of version 31) name the account. Ships in the same release as store version 30.
37
+ - **A deleted document keeps its tags** (store version 32; available from the cli-core release that carries store versions 30 to 33). The version-1 trigger that dropped a document's tags when it was marked deleted is gone; its tags go when the row itself is purged. `knowledge.labelled` and the vocabulary's `usageCount` leave out a deleted document's taggings, and search already did. A document revived by a later sync, such as a file moved to another folder, comes back with its tags.
38
+ - **Notes folders are accounts of provider `obsidian` (a vault) or `local` (a plain folder), no longer `folder`** (store version 32; available from the cli-core release that carries store versions 30 to 33). The migration renames each by its `settings.format`; A Drive stand-in folder (`google-drive:<principal>`) that version 31 left behind keeps `folder`. `FOLDER_PROVIDER` is replaced by `FOLDER_PROVIDERS` and `isFolderProvider`, both exported from the package root; `local` and `obsidian` are built-in providers of kind `notes`, and `--source obsidian` / `--source local` name the folders of that provider. Saved profile keys and annotation export files that say `folder` still resolve and import. **Version 32 raises `minCompatible` to 32**: a CLI on an older cli-core refuses the store and says to upgrade, instead of finding no folders and importing them a second time.
39
+
40
+ ### Added
41
+
42
+ - **`knowledge.applyTagConsolidation`** (available from the cli-core release that carries store versions 30 to 33) applies an owner-reviewed complete preview
43
+ atomically, retains independent assignment/claim/alias/link provenance, follows inherited names in reads and
44
+ search, and returns a text-free idempotent receipt. Label removal respects requested origin scope across retained
45
+ members. [The consolidation guide](docs/dev/TAG-CONSOLIDATION.md) describes review, compatibility and the explicit
46
+ transfer limitation.
47
+ - **Files: the source records of documents** (store version 32; available from the cli-core release that carries store versions 30 to 33). A new `files` table holds one row per item in one place: a file under a local folder or an Obsidian vault, a Drive file or folder. Each row has the item's `path` from its account's root, a `parent_id` to its folder's own row (local folders get rows made from the path), `path_folded` for prefix and substring search, and the `document_id` it is a copy of. A Drive item is keyed by its Drive file id and takes its link, type, size, edit time and version from the document's columns or, for rows saved before those columns, from its front matter; a Markdown file's front matter is `metadata.front_matter`. Every documents writer in cli-core keeps its row current (`saveFileNote`, `renameFileNote`, `deleteFileNotes`, `catalog.upsert`, `catalog.reconcile`, annotation transfers), and the first open after the upgrade writes the rows of every existing document in short batches, resuming where it stopped. `documents` stays the entity: no document is re-inserted and every `document:<id>` keeps working.
48
+ - **The owner pins the file a document shows** (store version 32; same release). `documents.pinned_file_id` (nullable) holds the owner's choice; `store.files.pin(document, fileId)` takes one of the document's own files and `store.files.unpin(document)` clears it, so the document shows its most recently changed file again. `FilesStore` from `@wirecat/cli-core/store`.
49
+ - **`EventStore`, the calendar's store** (`store.calendar`; types from `@wirecat/cli-core/meetings`; available from the cli-core release that carries store versions 30 to 33). `saveCalendars` (a calendar the owner owns syncs, others are listed), `calendars`, `setCalendarSynced`, `saveEntries` (one sync page in one transaction, matched by rules 1–5 of requirements 3.4: the same calendar and id; the same series slot, so a plain occurrence and its exception are one entry, in whichever order the series and its occurrences arrive; another calendar's copy by iCalUID; a Zoom meeting's automatic event by room and overlapping time, which the entry adopts as origin `calendar`; else a new event), `markUnseen` (after a full list; plain occurrences only inside the window fetched), `events(filter)` by time, account, calendar or person, `event(id)` with its entries, participants and revisions, `linkEntry` / `unlinkEntry` (the owner's, which no rule replaces), `reviews` and `resolveReview` (choose a candidate, or none of them). An event shows its owner-organized copy, else the latest edited; it is cancelled only when every live entry is. A deleted single event, occurrence or whole series keeps its past as history (`source_deleted_at`) and cancels its future. Each change of an entry's or meeting's event is logged in `entity_link_events`. Text is stored with HTML reduced to text, passcode, password and PIN lines dropped and every link without its query string; `metadata` keeps only `ENTRY_METADATA_KEYS`. `eventStoreContract` (`@wirecat/cli-core/meetings/testing`) runs the rules against any `EventStore`.
50
+ - **Calendars in the store** (store version 33; available from the cli-core release that carries store versions 30 to 33). `calendars` (an account's calendars, `synced` for those whose entries sync), `calendar_entries` (one calendar's copy of a single event, a series or one occurrence: the source records of `events` and `event_series`, with `ical_uid`, `original_starts_at`, `recurrence`, `recurrence_ends_at` — NULL for a series that repeats indefinitely —, `join_url` without its query string and `join_key`; a trigger refuses an entry that ends before it starts), `calendar_entry_revisions` (tracked fields before a change) and `calendar_entry_participants` (who an entry lists, by address). Events gain `status`, `all_day` and `source_deleted_at` (below). Events have a words index of their own (`event_words`, `event_stems`, queue `event_index_pending`) over title, description, location and attendee names, leaving out working-location, focus-time, out-of-office and birthday entries; every existing event is queued. Nothing writes calendars yet: `EventStore` comes next.
51
+ - **`meetings.join_key`** (store version 33; same release): the room a meeting is held in without its passcode — `zoom:<number>`, `meet:<code>`, else host and path — written with every meeting and filled on open for those stored before, so a calendar entry can be matched to its Zoom meeting by room rather than by a link carrying `pwd=`. `joinKeyOf` and `joinUrlWithoutQuery` (`src/meetings/join-key.ts`).
52
+ - **`store.files`, where each document is kept** (available from the cli-core release that carries store versions 30 to 33). `list(accountId, { under, pathContains, kind, includeDeleted, limit, offset })` reads one account's files by path: `under` is a subtree, one range over the folded path's index; `pathContains` is any case and accent. `byPath(accountId, path)` and `ofDocument(reference)`, which answers the most recently changed live file first: the one the document shows. Types `FilesStore`, `StoredFile` and `FileListQuery` from `@wirecat/cli-core/store`. The account is always named; there is no default place.
53
+ - **Search hits read a document's link and edit time from its file** (same release): `search notes`, `search all` and context evidence prefer the preferred file's `url` and `external_updated_at`, then the document's columns, then Drive's front matter. A document whose account's provider is registered with kind `documents` is a document with no front matter, labelled with the command whose profile names that account.
54
+ - **`identities.email`** (store version 32; same release). A mail contact's address, lower case, filled for every existing mail contact and by a trigger for every new one, whatever writes it. A contact saved with an address (`savePeople`'s `email`, a Drive owner or editor) carries it too, and the same-email join finds the mail contact by this column.
55
+ - **`entity_link_events` and `link_reviews`** (store version 32; same release): the log of every change of the entity a source record belongs to (each file's link to its document now, method `import`; calendar entries and meetings to events next), and the owner's review list of source records with two equally good entities, which nothing writes yet.
56
+ - **`NoteFolder.provider`**: `obsidian`, `local`, or `folder` for Drive's stand-in folders.
57
+
10
58
  ## 0.419.0 — 25.10.2026
11
59
 
12
60
  ### Fixed
@@ -1,7 +1,8 @@
1
1
  import type { Command } from "commander";
2
2
  import { SendJournal } from "../../sends/journal.js";
3
- import type { Level, PermissionKey } from "../../sends/permissions.js";
3
+ import { type Level, type PermissionKey } from "../../sends/permissions.js";
4
4
  import { RecipientList } from "../../sends/recipients.js";
5
+ import type { Settings } from "../settings.js";
5
6
  import type { BotAdapter, BotChatRef, BotConnectOptions, BotMessenger } from "./port.js";
6
7
  import { ChatRegistry } from "./registry.js";
7
8
  import { BotTokenStore } from "./token.js";
@@ -9,7 +10,7 @@ import { BotTokenStore } from "./token.js";
9
10
  export declare const botWords: (bot: BotMessenger, profile: string) => string;
10
11
  /** What a bot command needs, resolved once: the bot's settings, its token, its lists. */
11
12
  export declare const botContext: (command: Command, bot: BotMessenger) => {
12
- settings: import("../settings.js").Settings;
13
+ settings: Settings;
13
14
  renderer: import("@wirecat/cli-base").Renderer;
14
15
  format: import("@wirecat/cli-base").RenderFormat;
15
16
  color: boolean;
@@ -2,16 +2,30 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { pickChat } from "../../resolve.js";
3
3
  import { sendGuard } from "../../sends/guard.js";
4
4
  import { SendJournal } from "../../sends/journal.js";
5
+ import { levelFor, readKeysForCommand } from "../../sends/permissions.js";
5
6
  import { RecipientList } from "../../sends/recipients.js";
6
7
  import { readSecret } from "../../terminal/prompt.js";
8
+ import { commandPathOf } from "../command-contract.js";
7
9
  import { baseContext, secretInputOf } from "../context.js";
8
10
  import { terminalAsker } from "../messenger/ask.js";
11
+ import { refuseUnknownPermissionKeys } from "../permission-keys.js";
9
12
  import { asFirstWord } from "../profile.js";
10
13
  import { botCopy } from "./copy.js";
11
14
  import { botFiles, ChatRegistry } from "./registry.js";
12
15
  import { BotTokenStore } from "./token.js";
13
16
  /** How the owner types this bot — `max sales bot` — so a refusal names the command that fixes it. */
14
17
  export const botWords = (bot, profile) => `${bot.app.command} ${asFirstWord(profile)}bot`;
18
+ /** What a messenger command refuses before it connects, for a bot: an unknown key, then a denied read. */
19
+ const refuseBotPermissions = (command, bot, settings) => {
20
+ refuseUnknownPermissionKeys(command, settings.permissions, `${bot.app.command} ${asFirstWord(settings.profile)}config unset --bot`);
21
+ for (const key of readKeysForCommand(commandPathOf(command))) {
22
+ const { level, key: named } = levelFor(settings.permissions, key);
23
+ if (level !== "deny")
24
+ continue;
25
+ throw new CliError("permission_error", `bot profile ${settings.profile} denies ${key} (permissions.${named} is deny, from the ` +
26
+ `${settings.permissionSources[named ?? ""] ?? "default"})`, { permission: key });
27
+ }
28
+ };
15
29
  /** What a bot command needs, resolved once: the bot's settings, its token, its lists. */
16
30
  export const botContext = (command, bot) => {
17
31
  const base = baseContext(command, (flags, options) => bot.resolveSettings(flags, { ...options, kind: "bot" }));
@@ -21,6 +35,7 @@ export const botContext = (command, bot) => {
21
35
  const tokens = bot.tokenStore?.(command, profile) ??
22
36
  new BotTokenStore({ app: bot.app, profile, env, ...(keyring ? { keyring } : {}) });
23
37
  const words = botWords(bot, profile);
38
+ refuseBotPermissions(command, bot, settings);
24
39
  const registry = bot.registry?.(command, profile) ?? new ChatRegistry(bot.app, profile, env);
25
40
  const recipients = () => new RecipientList(files.recipients, words);
26
41
  const journal = () => new SendJournal(files.journal, { service: bot.app.command, version: bot.app.version });
@@ -55,7 +55,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
55
55
  .option("--bot", "only for bots — the bot section of the file");
56
56
  if (action === "set")
57
57
  sub
58
- .argument("<value>", "a number, true or false, or for allow a list like send,reaction; for a group tg:work,max:Alice,email:me@x")
58
+ .argument("<value>", "a number, true or false; for a group tg:work,max:Alice,email:me@x")
59
59
  .description("save a setting to the configuration file");
60
60
  else
61
61
  sub.description("remove a setting from the configuration file");
@@ -64,7 +64,6 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
64
64
  const context = baseContext(this, (flags, options) => config.resolveSettings(flags, {
65
65
  ...options,
66
66
  needsProfile: !everyone && setting !== "default_profile" && !isStoreSetting(setting),
67
- skipMovedPermissions: true,
68
67
  }));
69
68
  const { settings, renderer, env } = context;
70
69
  if (isStoreSetting(setting)) {
@@ -96,7 +96,6 @@ export type { Running, ServerOptions, ServerProcess, ServerSystem } from "./mess
96
96
  export { storeSummary } from "./messenger/store-maintenance-command.js";
97
97
  export { type Saving, stored } from "./messenger/stored.js";
98
98
  export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
99
- export { movedCommandError } from "./moved.js";
100
99
  export { window, withPaging } from "./paging.js";
101
100
  export { knownBeside, knownPermissionKeys, type PermissionKeyOf, unknownPermissionKeys } from "./permission-keys.js";
102
101
  export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
package/dist/cli/index.js CHANGED
@@ -87,7 +87,6 @@ export { STEMS_EVERY_MS, STEMS_SLICE_MS, stemFills } from "./messenger/serve-ste
87
87
  export { storeSummary } from "./messenger/store-maintenance-command.js";
88
88
  export { stored } from "./messenger/stored.js";
89
89
  export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
90
- export { movedCommandError } from "./moved.js";
91
90
  export { window, withPaging } from "./paging.js";
92
91
  export { knownBeside, knownPermissionKeys, unknownPermissionKeys } from "./permission-keys.js";
93
92
  export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
@@ -3,12 +3,12 @@ import { servingProfiles } from "../../background/lock.js";
3
3
  import { FloodMemory, floodPathFor } from "../../sends/flood.js";
4
4
  import { ceilingRefusal, guardFor } from "../../sends/guard.js";
5
5
  import { DEFAULT_PACE, Pacer, pacePathFor } from "../../sends/pace.js";
6
- import { assertStatsPermissionsCurrent, CEILING_KEY, levelFor, readKeysForCommand, } from "../../sends/permissions.js";
6
+ import { CEILING_KEY, levelFor, readKeysForCommand } from "../../sends/permissions.js";
7
7
  import { RecipientList, recipientsPathFor } from "../../sends/recipients.js";
8
8
  import { OFFLINE, servicesFor } from "../../services/index.js";
9
9
  import { openStore } from "../../store/store.js";
10
10
  import { baseContext, environmentOf } from "../context.js";
11
- import { knownPermissionKeys, unknownPermissionKeys } from "../permission-keys.js";
11
+ import { refuseUnknownPermissionKeys } from "../permission-keys.js";
12
12
  import { asFirstWord } from "../profile.js";
13
13
  import { rootOf } from "../steps.js";
14
14
  import { recalledAccount, rememberAccount } from "./accounts.js";
@@ -325,7 +325,6 @@ const pathOf = (command) => {
325
325
  };
326
326
  /** `deny` stops a read too, before anything connects: what the profile may not see is never fetched. */
327
327
  const refuseDenied = (command, settings) => {
328
- assertStatsPermissionsCurrent(pathOf(command), settings.permissions);
329
328
  for (const key of readKeysForCommand(pathOf(command))) {
330
329
  const { level, key: named } = levelFor(settings.permissions, key);
331
330
  if (level !== "deny")
@@ -356,9 +355,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
356
355
  const { profile } = base.settings;
357
356
  refuseDenied(command, base.settings);
358
357
  refuseLacking(messenger, pathOf(command));
359
- for (const key of unknownPermissionKeys(base.settings.permissions, knownPermissionKeys(command, messenger.permissionKey))) {
360
- throw new CliError("configuration_error", `permissions.${key} names no command — remove it with \`${app.command} ${asFirstWord(profile)}config unset permissions.${key}\``, { permission: key });
361
- }
358
+ refuseUnknownPermissionKeys(command, base.settings.permissions, `${app.command} ${asFirstWord(profile)}config unset`, messenger.permissionKey);
362
359
  // Read when warning, not now: serve swaps the renderer's methods for log records after this.
363
360
  const warn = (text, warning) => base.renderer.warn(text, warning);
364
361
  const accountFor = () => {
@@ -9,3 +9,5 @@ export declare const knownPermissionKeys: (command: Command, keyOf?: PermissionK
9
9
  /** The known keys one level under the unknown key's parent, to name in the refusal. */
10
10
  export declare const knownBeside: (key: PermissionKey, known: ReadonlySet<PermissionKey>) => PermissionKey[];
11
11
  export declare const unknownPermissionKeys: (levels: Readonly<Record<PermissionKey, unknown>>, known: ReadonlySet<PermissionKey>) => PermissionKey[];
12
+ /** A saved key no command checks would do nothing, so the command refuses to run; `unset` names the fix. */
13
+ export declare const refuseUnknownPermissionKeys: (command: Command, levels: Readonly<Record<PermissionKey, unknown>>, unset: string, keyOf?: PermissionKeyOf) => void;
@@ -1,4 +1,4 @@
1
- import { canonicalPermissionKey } from "../sends/permission-aliases.js";
1
+ import { CliError } from "@wirecat/cli-base";
2
2
  import { CEILING_KEY, keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
3
3
  const pathOf = (command) => {
4
4
  const words = [];
@@ -20,12 +20,12 @@ const everyCommand = (command) => [command, ...command.commands.flatMap(everyCom
20
20
  export const knownPermissionKeys = (command, keyOf = (one) => keyForCommand(pathOf(one)), extra = []) => {
21
21
  const known = new Set();
22
22
  const add = (key) => {
23
- key = canonicalPermissionKey(key);
24
23
  for (let parts = key.split("."); parts.length > 0; parts = parts.slice(0, -1))
25
24
  known.add(parts.join("."));
26
25
  };
27
26
  for (const one of everyCommand(rootOf(command))) {
28
- const key = keyOf(one);
27
+ const own = keyOf(one);
28
+ const key = own === undefined ? keyForCommand(pathOf(one)) : own;
29
29
  if (key)
30
30
  add(key);
31
31
  }
@@ -42,3 +42,10 @@ export const knownBeside = (key, known) => {
42
42
  .sort();
43
43
  };
44
44
  export const unknownPermissionKeys = (levels, known) => Object.keys(levels).filter((key) => key !== CEILING_KEY && !known.has(key));
45
+ /** A saved key no command checks would do nothing, so the command refuses to run; `unset` names the fix. */
46
+ export const refuseUnknownPermissionKeys = (command, levels, unset, keyOf) => {
47
+ const [key] = unknownPermissionKeys(levels, knownPermissionKeys(command, keyOf));
48
+ if (key === undefined)
49
+ return;
50
+ throw new CliError("configuration_error", `permissions.${key} names no command — remove it with \`${unset} permissions.${key}\``, { permission: key });
51
+ };
@@ -1,8 +1,8 @@
1
1
  import type { Command } from "commander";
2
- import { type Level } from "../sends/permissions.js";
2
+ import { type Level, type PermissionKey } from "../sends/permissions.js";
3
3
  export declare class PreviewComplete extends Error {
4
4
  }
5
- export declare const preview: (command: Command, permissions: Readonly<Record<string, Level>>) => {
5
+ export declare const preview: (command: Command, permissions: Readonly<Record<string, Level>>, unset: string, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined) => {
6
6
  preview: boolean;
7
7
  path: string[];
8
8
  permission: {
@@ -1,15 +1,16 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { metaOf } from "@wirecat/cli-base/commands";
3
3
  import { writes as writesAnything } from "@wirecat/cli-base/standards";
4
- import { assertStatsPermissionsCurrent, keyForCommand, levelFor, readKeysForCommand, WRITE_KEYS, } from "../sends/permissions.js";
4
+ import { keyForCommand, levelFor, readKeysForCommand, WRITE_KEYS, } from "../sends/permissions.js";
5
5
  import { commandPathOf } from "./command-contract.js";
6
+ import { refuseUnknownPermissionKeys } from "./permission-keys.js";
6
7
  export class PreviewComplete extends Error {
7
8
  }
8
- export const preview = (command, permissions) => {
9
+ export const preview = (command, permissions, unset, keyOf) => {
9
10
  const path = commandPathOf(command);
11
+ refuseUnknownPermissionKeys(command, permissions, unset, keyOf && ((one) => keyOf(commandPathOf(one))));
10
12
  const key = keyForCommand(path);
11
13
  const level = key ? levelFor(permissions, key).level : "allow";
12
- assertStatsPermissionsCurrent(path, permissions);
13
14
  for (const source of readKeysForCommand(path)) {
14
15
  if (levelFor(permissions, source).level === "deny")
15
16
  throw new CliError("permission_error", `this profile does not permit ${source}`, { permission: source });
@@ -25,9 +25,8 @@ import { installHelp } from "./help/first-screen.js";
25
25
  import { showInputFields } from "./input-fields.js";
26
26
  import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
27
27
  import { checkJson, liftJson } from "./json-fields.js";
28
- import { movedCommandError } from "./moved.js";
29
28
  import { PreviewComplete, preview } from "./preview.js";
30
- import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
29
+ import { asFirstWord, commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
31
30
  import { absolutePaths, relayTarget } from "./relay-route.js";
32
31
  import { settingsFor } from "./settings.js";
33
32
  import { assertSharedContracts } from "./shared-contracts.js";
@@ -377,7 +376,9 @@ const runOnce = async (argv, definition, options) => {
377
376
  needsProfile: keyForCommand(path) !== null,
378
377
  });
379
378
  const permissions = "permissions" in resolved ? resolved.permissions : {};
380
- renderAnswer(action, outputFor(action), itemOf(snakeKeys(preview(action, permissions))));
379
+ const unset = `${command} ${asFirstWord(resolved.profile)}config unset${path[0] === "bot" ? " --bot" : ""}`;
380
+ const previewed = preview(action, permissions, unset, definition.configuration?.permissionKey);
381
+ renderAnswer(action, outputFor(action), itemOf(snakeKeys(previewed)));
381
382
  throw new PreviewComplete();
382
383
  }
383
384
  // Not inside the scheduled run itself: its own failure is already on its way to last.json.
@@ -421,11 +422,6 @@ const runOnce = async (argv, definition, options) => {
421
422
  const settled = await settleFailure(failure, { definition, program, rest, profile, options });
422
423
  return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
423
424
  }
424
- const failure = movedCommandError(program, rest);
425
- if (failure !== undefined) {
426
- const settled = await settleFailure(failure, { definition, program, rest, profile, options });
427
- return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
428
- }
429
425
  try {
430
426
  await control.race(() => program.parseAsync(rest, { from: "user" }));
431
427
  const code = exit.code ?? (process.exitCode === undefined ? 0 : Number(process.exitCode));
@@ -140,8 +140,6 @@ export interface ResolveOptions {
140
140
  needsProfile?: boolean;
141
141
  /** Read the config file without writing it: no default file. For a dry run. */
142
142
  readOnly?: boolean;
143
- /** Skip moved permission keys instead of refusing them, so `config set` and `unset` can fix one. */
144
- skipMovedPermissions?: boolean;
145
143
  }
146
144
  /** Where `profile` came from when none was named and the command needs none. */
147
145
  export declare const NONE_NAMED = "none named";
@@ -4,7 +4,6 @@ import * as v from "valibot";
4
4
  import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
5
5
  import { parseDuration } from "../domain/duration.js";
6
6
  import { envName, readOnlyLockOf } from "../ports/app.js";
7
- import { canonicalPermissionKey } from "../sends/permission-aliases.js";
8
7
  import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
9
8
  import { storePath } from "../store/path.js";
10
9
  import { sharedConfigPath } from "../store/shared-config.js";
@@ -78,7 +77,6 @@ const SHARED_DEFAULTS_ONLY = {
78
77
  update_check: v.optional(flag),
79
78
  skill_hint: v.optional(flag),
80
79
  };
81
- const withoutMoved = (levels) => levels && Object.fromEntries(Object.entries(levels).filter(([key]) => canonicalPermissionKey(key) === key));
82
80
  /** Where `profile` came from when none was named and the command needs none. */
83
81
  export const NONE_NAMED = "none named";
84
82
  /** A name for a command that speaks for no account; never chosen for one that does. */
@@ -232,9 +230,7 @@ export const settingsFor = (app, extension = {}) => {
232
230
  : { value: false, from: "default" };
233
231
  const fileLayers = layers.map(([from, scope]) => [
234
232
  from,
235
- options.skipMovedPermissions
236
- ? withoutMoved(scope?.permissions)
237
- : scope?.permissions,
233
+ scope?.permissions,
238
234
  ]);
239
235
  const overrides = permissionOverrides(flags.permission);
240
236
  assertOverridesTighten(overrides, layerPermissions(fileLayers).levels);
@@ -11,8 +11,26 @@ export interface ProviderEntry {
11
11
  }
12
12
  /** The seed: what cli-core knows before any CLI has written its entry into the store. */
13
13
  export declare const PROVIDERS: readonly ProviderEntry[];
14
- /** The store keeps a notes folder as an `accounts` row of this provider; people call it `notes`. */
15
- export declare const FOLDER_PROVIDER = "folder";
14
+ /**
15
+ * A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
16
+ * `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
17
+ * profile keys saved before that still say.
18
+ */
19
+ export declare const FOLDER_PROVIDERS: readonly ["local", "obsidian", "folder"];
20
+ export declare const isFolderProvider: (provider: string) => boolean;
21
+ /** The same test in SQL, over an `accounts` row's `provider` column. */
22
+ export declare const folderProviderSql: (column?: string) => string;
23
+ /** The provider a new folder of this format gets. */
24
+ export declare const folderProviderOf: (format: "obsidian" | "markdown") => "obsidian" | "local";
25
+ /**
26
+ * Providers whose account holds documents the way a notes folder does: one account per remote principal, its
27
+ * files as `documents` rows (store version 31 moved Drive here from a `folder` named `google-drive:<principal>`).
28
+ * The provider registry's `documents` kind replaces this list when Drive leaves `notes` (Wave 3 R2).
29
+ */
30
+ export declare const DOCUMENT_ACCOUNT_PROVIDERS: readonly ["google-drive"];
31
+ export declare const isDocumentAccountProvider: (provider: string) => boolean;
32
+ /** A folder or a documents account, in SQL over an `accounts` row's `provider` column. */
33
+ export declare const filesAccountSql: (column?: string) => string;
16
34
  /** Notes written in memo have no account row; this is their account. */
17
35
  export declare const INTERNAL_NOTES = "internal";
18
36
  /** The reason a search gives for an account whose provider no entry names. */
@@ -11,10 +11,30 @@ export const PROVIDERS = [
11
11
  { name: "whatsapp-bot", kind: "messages", aliases: ["wa-bot"] },
12
12
  { name: "email", kind: "mail", aliases: [] },
13
13
  { name: "notes", kind: "notes", aliases: [] },
14
+ { name: "local", kind: "notes", aliases: [] },
15
+ { name: "obsidian", kind: "notes", aliases: [] },
14
16
  { name: "zoom", kind: "meetings", aliases: [] },
15
17
  ];
16
- /** The store keeps a notes folder as an `accounts` row of this provider; people call it `notes`. */
17
- export const FOLDER_PROVIDER = "folder";
18
+ /**
19
+ * A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
20
+ * `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
21
+ * profile keys saved before that still say.
22
+ */
23
+ export const FOLDER_PROVIDERS = ["local", "obsidian", "folder"];
24
+ export const isFolderProvider = (provider) => FOLDER_PROVIDERS.includes(provider);
25
+ /** The same test in SQL, over an `accounts` row's `provider` column. */
26
+ export const folderProviderSql = (column = "provider") => `${column} IN (${FOLDER_PROVIDERS.map((name) => `'${name}'`).join(", ")})`;
27
+ /** The provider a new folder of this format gets. */
28
+ export const folderProviderOf = (format) => format === "markdown" ? "local" : "obsidian";
29
+ /**
30
+ * Providers whose account holds documents the way a notes folder does: one account per remote principal, its
31
+ * files as `documents` rows (store version 31 moved Drive here from a `folder` named `google-drive:<principal>`).
32
+ * The provider registry's `documents` kind replaces this list when Drive leaves `notes` (Wave 3 R2).
33
+ */
34
+ export const DOCUMENT_ACCOUNT_PROVIDERS = ["google-drive"];
35
+ export const isDocumentAccountProvider = (provider) => DOCUMENT_ACCOUNT_PROVIDERS.includes(provider);
36
+ /** A folder or a documents account, in SQL over an `accounts` row's `provider` column. */
37
+ export const filesAccountSql = (column = "provider") => `${column} IN (${[...FOLDER_PROVIDERS, ...DOCUMENT_ACCOUNT_PROVIDERS].map((name) => `'${name}'`).join(", ")})`;
18
38
  /** Notes written in memo have no account row; this is their account. */
19
39
  export const INTERNAL_NOTES = "internal";
20
40
  /** The reason a search gives for an account whose provider no entry names. */
@@ -105,6 +125,6 @@ export const providerNamed = (word) => {
105
125
  return registry.find((entry) => wordsOf(entry).includes(wanted));
106
126
  };
107
127
  /** The kind of a provider the registry knows; `undefined` for one no entry names, which a search skips. */
108
- export const kindIn = (provider) => provider === FOLDER_PROVIDER ? "notes" : registry.find(({ name }) => name === provider)?.kind;
128
+ export const kindIn = (provider) => isFolderProvider(provider) ? "notes" : registry.find(({ name }) => name === provider)?.kind;
109
129
  /** A provider the registry does not list counts as a messenger here; a search asks `kindIn` and skips it. */
110
130
  export const kindOf = (provider) => kindIn(provider) ?? "messages";
@@ -16,6 +16,8 @@ export interface TagConsolidationPreview {
16
16
  id: string;
17
17
  name: string;
18
18
  };
19
+ sourceMemberIds: string[];
20
+ survivorMemberIds: string[];
19
21
  vocabularyFingerprint: string;
20
22
  previewFingerprint: string;
21
23
  assignments: {
@@ -69,9 +71,36 @@ export interface TagConsolidationPreview {
69
71
  rows: number;
70
72
  readBytes: number;
71
73
  samplesComplete: boolean;
72
- applySupported: false;
74
+ applySupported: true;
73
75
  };
74
76
  }
75
77
  export interface TagConsolidationStore {
76
78
  previewTagConsolidation(options: TagConsolidationOptions): Promise<TagConsolidationPreview>;
79
+ applyTagConsolidation(options: TagConsolidationApplyOptions): Promise<TagConsolidationReceipt>;
80
+ }
81
+ export interface TagConsolidationApplyOptions extends Omit<TagConsolidationOptions, "sampleLimit"> {
82
+ by: "owner";
83
+ expectedPreviewFingerprint: string;
84
+ idempotencyKey: string;
85
+ }
86
+ export interface TagConsolidationReceipt {
87
+ id: string;
88
+ sourceId: string;
89
+ survivorId: string;
90
+ sourceMemberIds: string[];
91
+ survivorMemberIds: string[];
92
+ actor: {
93
+ type: "person";
94
+ id: string;
95
+ };
96
+ createdAt: string;
97
+ idempotencyKey: string;
98
+ vocabularyFingerprint: string;
99
+ previewFingerprint: string;
100
+ counts: {
101
+ assignments: number;
102
+ aliases: number;
103
+ automaticClaims: number;
104
+ links: number;
105
+ };
77
106
  }
@@ -6783,7 +6783,10 @@ export declare const ENTITIES: {
6783
6783
  readonly starts_at: v.NullableSchema<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`">]>, undefined>;
6784
6784
  readonly ends_at: v.NullableSchema<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`">]>, undefined>;
6785
6785
  readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
6786
- readonly origin: v.PicklistSchema<["auto", "owner"], undefined>;
6786
+ readonly origin: v.PicklistSchema<["auto", "owner", "calendar"], undefined>;
6787
+ readonly status: v.NullableSchema<v.StringSchema<undefined>, undefined>;
6788
+ readonly all_day: v.BooleanSchema<undefined>;
6789
+ readonly source_deleted_at: v.NullableSchema<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`">]>, undefined>;
6787
6790
  readonly deleted_at: v.NullableSchema<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`">]>, undefined>;
6788
6791
  readonly created_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`">]>;
6789
6792
  readonly updated_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`">]>;
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { parseMarkdownSpans } from "./domain/markdown-spans.js";
8
8
  export type { MessageLink, MessagePermalink } from "./domain/message-link.js";
9
9
  export type * from "./domain/models.js";
10
10
  export { countedPoll } from "./domain/polls.js";
11
- export { isKind, KINDS, type Kind, kindIn, kindOf, PROVIDERS, type ProviderEntry, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
11
+ export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, type Kind, kindIn, kindOf, PROVIDERS, type ProviderEntry, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
12
12
  export type { RankingGraphEvidence, RankingGraphLink } from "./domain/rankings-graph.js";
13
13
  export type { RankingComponent, RankingInput, RankingMeasure, RankingOptions, RankingTarget, RankingWeights, ScorePreset, } from "./domain/rankings-options.js";
14
14
  export { rankingOptions } from "./domain/rankings-options.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { formatLocator, isLocator, parseLocator } from "./domain/locator.js";
5
5
  export { parseMarkdown } from "./domain/markdown.js";
6
6
  export { parseMarkdownSpans } from "./domain/markdown-spans.js";
7
7
  export { countedPoll } from "./domain/polls.js";
8
- export { isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
8
+ export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
9
9
  export { rankingOptions } from "./domain/rankings-options.js";
10
10
  export { canonicalReference, formatReference, parseReference } from "./domain/references.js";
11
11
  export { normalizeTag, TAG_TYPES } from "./domain/tags.js";
@@ -2,7 +2,6 @@ import { toJsonSchema, toStandardJsonSchema } from "@valibot/to-json-schema";
2
2
  import { CliError } from "@wirecat/cli-base";
3
3
  import * as v from "valibot";
4
4
  import { answerSchema, listOf } from "../cli/envelope.js";
5
- import { movedPath, movedPointer } from "../cli/moved.js";
6
5
  import { ENTITIES } from "../entities/index.js";
7
6
  import { agentArguments } from "./text.js";
8
7
  import { answered, commandOf, entryRunner, failed, inputOf, outputSchemaOf, READ, syncAllowedFor, UNTRUSTED, WRITE, } from "./tool.js";
@@ -103,9 +102,6 @@ withheld) => {
103
102
  const one = command === undefined ? undefined : commands[command];
104
103
  if (command === undefined || !one) {
105
104
  withheld?.(normal(asked));
106
- const moved = movedPath(normal(asked).split(" "), (path) => byNormal.has(path));
107
- if (moved !== undefined)
108
- throw new CliError("validation_error", `no command "${asked}" here — ${movedPointer(moved)}`);
109
105
  const near = nearest(commands, asked);
110
106
  throw new CliError("validation_error", `no command "${asked}" here${near.length ? ` — did you mean ${near.map((name) => `"${name}"`).join(", ")}?` : ""}; ${prefix}_tools_search finds one`);
111
107
  }
@@ -10,7 +10,7 @@ import { type EntityName } from "../entities/index.js";
10
10
  import { type AppErrors } from "../errors/reasons.js";
11
11
  import type { MessengerAdapter } from "../ports/messenger.js";
12
12
  import type { SendGuard } from "../sends/guard.js";
13
- import { type Permission, type PermissionKey } from "../sends/permissions.js";
13
+ import { type PermissionKey } from "../sends/permissions.js";
14
14
  import { type Services } from "../services/index.js";
15
15
  import type { AccountKey, MessageStore } from "../store/store.js";
16
16
  import type { MessengerSession } from "./session.js";
@@ -50,8 +50,6 @@ interface Tool<S extends Input> {
50
50
  /** What it touches, where its hints cannot say: a read of the messenger kept in the store is not a write there. */
51
51
  effects?: Effects;
52
52
  _meta?: Record<string, unknown>;
53
- /** A write, and what the profile's `allow` must name for it to be offered. */
54
- permission?: Permission;
55
53
  /** Writes what the owner keeps, as its command (`isStoreChange`): `<PREFIX>_READ_ONLY` refuses it. */
56
54
  changesStore?: true;
57
55
  /** The CLI command it answers as, where its name does not spell it: `chats send-as`. Its level is read from it. */
package/dist/mcp/tool.js CHANGED
@@ -12,7 +12,7 @@ import { failureContext } from "../errors/reasons.js";
12
12
  import { partialOutcome, startRecording } from "../runs/recording.js";
13
13
  import { newRunId } from "../runs/run.js";
14
14
  import { inRun } from "../runs/scope.js";
15
- import { assertStatsPermissionsCurrent, keyForCommand, levelFor, readKeysForCommand, } from "../sends/permissions.js";
15
+ import { keyForCommand, levelFor, readKeysForCommand } from "../sends/permissions.js";
16
16
  import { onlineDeps, storeModeDeps } from "../services/deps.js";
17
17
  import { servicesFor } from "../services/index.js";
18
18
  import { agentJson } from "./text.js";
@@ -178,7 +178,6 @@ const callRunner = ({ messenger, session, withStore, withServices, defaults, aro
178
178
  definition.check?.(args);
179
179
  if (definition.changesStore)
180
180
  refuseStoreChange(messenger.app, defaults.env);
181
- assertStatsPermissionsCurrent(path, defaults.settings.permissions ?? {});
182
181
  refuseLacking(messenger, path);
183
182
  for (const permission of readKeysForCommand(path)) {
184
183
  if (levelFor(defaults.settings.permissions ?? {}, permission).level === "deny")
@@ -0,0 +1,13 @@
1
+ /** Every web link without its query string or fragment, which is where passcodes ride (`pwd=`). */
2
+ export declare const withoutLinkQueries: (text: string) => string;
3
+ /**
4
+ * A calendar's text as the store keeps it: HTML reduced to text, links without their query strings, and each line
5
+ * naming a passcode, password or PIN dropped — except from a title, where "PIN reset review" is a meeting's name.
6
+ * `null` when nothing is left.
7
+ */
8
+ export declare const calendarText: (text: string | null | undefined, { dropSecretLines }?: {
9
+ dropSecretLines?: boolean;
10
+ }) => string | null;
11
+ /** The provider fields a calendar entry may keep in `metadata`: none of them can open a meeting. */
12
+ export declare const ENTRY_METADATA_KEYS: readonly ["colorId", "guestsCanInviteOthers", "guestsCanModify", "guestsCanSeeOtherGuests", "hangoutLink", "locked", "privateCopy", "recurring_event_id"];
13
+ export declare const entryMetadata: (metadata: Record<string, unknown> | null | undefined) => Record<string, unknown>;