@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.
- package/CHANGELOG.md +48 -0
- package/dist/cli/bot/context.d.ts +3 -2
- package/dist/cli/bot/context.js +15 -0
- package/dist/cli/config-command.js +1 -2
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/cli/messenger/context.js +3 -6
- package/dist/cli/permission-keys.d.ts +2 -0
- package/dist/cli/permission-keys.js +10 -3
- package/dist/cli/preview.d.ts +2 -2
- package/dist/cli/preview.js +4 -3
- package/dist/cli/program.js +4 -8
- package/dist/cli/settings.d.ts +0 -2
- package/dist/cli/settings.js +1 -5
- package/dist/domain/providers.d.ts +20 -2
- package/dist/domain/providers.js +23 -3
- package/dist/domain/tag-consolidation.d.ts +30 -1
- package/dist/entities/index.d.ts +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/surface.js +0 -4
- package/dist/mcp/tool.d.ts +1 -3
- package/dist/mcp/tool.js +1 -2
- package/dist/meetings/calendar-text.d.ts +13 -0
- package/dist/meetings/calendar-text.js +57 -0
- package/dist/meetings/event-store.d.ts +202 -0
- package/dist/meetings/event-store.js +1 -0
- package/dist/meetings/events.js +5 -1
- package/dist/meetings/index.d.ts +3 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/join-key.d.ts +7 -0
- package/dist/meetings/join-key.js +38 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/model.d.ts +8 -2
- package/dist/meetings/read-answers.d.ts +6 -2
- package/dist/meetings/read-answers.js +9 -2
- package/dist/meetings/store.d.ts +2 -0
- package/dist/meetings/testing/event-store-contract.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +161 -0
- package/dist/meetings/testing/index.d.ts +1 -0
- package/dist/meetings/testing/index.js +1 -0
- package/dist/parity/manifest.js +0 -4
- package/dist/search/account-list.js +39 -7
- package/dist/sends/index.d.ts +1 -1
- package/dist/sends/index.js +1 -1
- package/dist/sends/permissions.d.ts +0 -13
- package/dist/sends/permissions.js +15 -142
- package/dist/services/agenda.js +5 -5
- package/dist/services/document-source.d.ts +7 -3
- package/dist/services/document-source.js +24 -12
- package/dist/services/documents/ingestion.d.ts +6 -1
- package/dist/services/documents/ingestion.js +4 -0
- package/dist/services/notes-search.js +3 -2
- package/dist/services/search-all.js +1 -0
- package/dist/services/task-tag-filters.d.ts +3 -0
- package/dist/services/task-tag-filters.js +8 -0
- package/dist/services/tasks.js +5 -9
- package/dist/store/document-transfer/index.js +12 -2
- package/dist/store/index.d.ts +2 -1
- package/dist/store/sqlite/accounts.js +1 -0
- package/dist/store/sqlite/calendar.d.ts +3 -0
- package/dist/store/sqlite/calendar.js +593 -0
- package/dist/store/sqlite/catalog.js +16 -5
- package/dist/store/sqlite/chat-rekey.js +1 -1
- package/dist/store/sqlite/chats.js +2 -2
- package/dist/store/sqlite/context-document-source.js +8 -4
- package/dist/store/sqlite/document-transfer/apply.js +24 -2
- package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
- package/dist/store/sqlite/document-transfer/collection.js +66 -0
- package/dist/store/sqlite/document-transfer/plan.js +15 -8
- package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
- package/dist/store/sqlite/entity-types.d.ts +5 -1
- package/dist/store/sqlite/entity-types.js +5 -1
- package/dist/store/sqlite/events.d.ts +8 -2
- package/dist/store/sqlite/events.js +36 -12
- package/dist/store/sqlite/files.d.ts +93 -0
- package/dist/store/sqlite/files.js +293 -0
- package/dist/store/sqlite/folded.d.ts +3 -0
- package/dist/store/sqlite/folded.js +1 -0
- package/dist/store/sqlite/imported-documents.js +1 -1
- package/dist/store/sqlite/join-keys.d.ts +8 -0
- package/dist/store/sqlite/join-keys.js +27 -0
- package/dist/store/sqlite/knowledge.js +6 -6
- package/dist/store/sqlite/link-reviews.d.ts +9 -0
- package/dist/store/sqlite/link-reviews.js +12 -0
- package/dist/store/sqlite/lucene-fields.js +8 -6
- package/dist/store/sqlite/manifest.js +11 -0
- package/dist/store/sqlite/meeting-selection.js +1 -1
- package/dist/store/sqlite/meetings.js +6 -1
- package/dist/store/sqlite/migrations.generated.js +143 -0
- package/dist/store/sqlite/note-index.d.ts +8 -1
- package/dist/store/sqlite/note-index.js +14 -0
- package/dist/store/sqlite/note-search.js +6 -5
- package/dist/store/sqlite/notes.d.ts +18 -1
- package/dist/store/sqlite/notes.js +237 -40
- package/dist/store/sqlite/open-store.js +4 -0
- package/dist/store/sqlite/person-links.js +7 -2
- package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
- package/dist/store/sqlite/schema/canonical-tags.js +8 -0
- package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
- package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
- package/dist/store/sqlite/schema.d.ts +3589 -1634
- package/dist/store/sqlite/schema.js +191 -13
- package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
- package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
- package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
- package/dist/store/sqlite/tag-consolidation-input.js +52 -0
- package/dist/store/sqlite/tag-consolidation.js +51 -66
- package/dist/store/sqlite/tag-creation.js +17 -8
- package/dist/store/sqlite/tag-resolution.d.ts +5 -0
- package/dist/store/sqlite/tag-resolution.js +33 -0
- package/dist/store/sqlite/tags.d.ts +3 -1
- package/dist/store/sqlite/tags.js +70 -50
- package/dist/store/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +6 -1
- package/dist/store/sqlite/things.js +2 -1
- package/dist/store/sqlite/vocabulary.d.ts +1 -0
- package/dist/store/sqlite/vocabulary.js +30 -8
- package/dist/store/store.d.ts +6 -0
- package/dist/store/store.js +10 -0
- package/dist/store/tags.js +2 -2
- package/package.json +2 -1
- package/dist/cli/moved.d.ts +0 -18
- package/dist/cli/moved.js +0 -72
- package/dist/sends/permission-aliases.d.ts +0 -5
- 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
|
|
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:
|
|
13
|
+
settings: Settings;
|
|
13
14
|
renderer: import("@wirecat/cli-base").Renderer;
|
|
14
15
|
format: import("@wirecat/cli-base").RenderFormat;
|
|
15
16
|
color: boolean;
|
package/dist/cli/bot/context.js
CHANGED
|
@@ -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
|
|
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)) {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
+
};
|
package/dist/cli/preview.d.ts
CHANGED
|
@@ -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: {
|
package/dist/cli/preview.js
CHANGED
|
@@ -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 {
|
|
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 });
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
-
|
|
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));
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -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";
|
package/dist/cli/settings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
15
|
-
|
|
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. */
|
package/dist/domain/providers.js
CHANGED
|
@@ -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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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:
|
|
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
|
}
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/mcp/surface.js
CHANGED
|
@@ -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
|
}
|
package/dist/mcp/tool.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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>;
|