@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
@@ -0,0 +1,161 @@
1
+ import assert from "node:assert/strict";
2
+ const HOUR = 3_600_000;
3
+ /** "Now" in every case: entries before it have happened. */
4
+ export const CONTRACT_NOW = 1_000 * HOUR;
5
+ const single = (externalId, at, extra = {}) => ({
6
+ externalId,
7
+ kind: "single",
8
+ status: "confirmed",
9
+ title: "Example review",
10
+ startsAt: at,
11
+ endsAt: at + HOUR,
12
+ ...extra,
13
+ });
14
+ export const eventStoreContract = (make) => {
15
+ const test = (name, check) => ({
16
+ name,
17
+ async run() {
18
+ const fixture = await make();
19
+ const [calendar] = await fixture.store.saveCalendars(fixture.accountId, [{ externalId: "primary", title: "Work", timezone: "UTC", accessRole: "owner", primary: true }], 1);
20
+ await check({ ...fixture, calendar: calendar.id });
21
+ },
22
+ });
23
+ const second = async ({ store, accountId }, externalId = "personal") => (await store.saveCalendars(accountId, [{ externalId, title: externalId, timezone: "UTC", accessRole: "owner", primary: false }], 1))[0].id;
24
+ const later = CONTRACT_NOW + 24 * HOUR;
25
+ const earlier = CONTRACT_NOW - 24 * HOUR;
26
+ return [
27
+ test("a calendar the owner owns syncs; one shared with them is listed and does not", async ({ store, accountId, }) => {
28
+ await store.saveCalendars(accountId, [{ externalId: "team", title: "Team", timezone: null, accessRole: "reader", primary: false }], 2);
29
+ assert.deepEqual((await store.calendars(accountId)).map(({ externalId, synced }) => [externalId, synced]), [
30
+ ["primary", true],
31
+ ["team", false],
32
+ ]);
33
+ }),
34
+ test("rule 1: the same calendar and id is the same entry; a change keeps the old values", async ({ store, calendar, }) => {
35
+ const [first] = await store.saveEntries(calendar, [single("e1", later)], 2);
36
+ const [again] = await store.saveEntries(calendar, [single("e1", later + HOUR, { title: "Moved review" })], 3);
37
+ assert.equal(again?.id, first?.id);
38
+ assert.equal(again?.eventId, first?.eventId);
39
+ const details = await store.event(first?.eventId);
40
+ assert.equal(details?.event.title, "Moved review");
41
+ assert.equal(details?.event.startsAt, later + HOUR);
42
+ assert.equal(details?.event.origin, "calendar");
43
+ assert.deepEqual(details?.revisions.map(({ title, startsAt }) => [title, startsAt]), [["Example review", later]]);
44
+ }),
45
+ test("rule 2: a plain occurrence and the exception of its slot are one entry, the series in any order", async ({ store, calendar, }) => {
46
+ const slot = later;
47
+ const occurrence = (externalId, extra = {}) => single(externalId, slot, { kind: "occurrence", seriesExternalId: "s1", originalStartsAt: slot, ...extra });
48
+ const [plain] = await store.saveEntries(calendar, [occurrence("s1_slot")], 2);
49
+ await store.saveEntries(calendar, [{ ...single("s1", earlier), kind: "series", recurrence: ["RRULE:FREQ=WEEKLY"], recurrenceEndsAt: null }], 3);
50
+ const [exception] = await store.saveEntries(calendar, [occurrence("s1_moved", { exception: true, startsAt: slot + HOUR, endsAt: slot + 2 * HOUR })], 4);
51
+ assert.equal(exception?.id, plain?.id);
52
+ assert.notEqual(exception?.seriesEntryId, null);
53
+ const details = await store.event(exception?.eventId);
54
+ assert.equal(details?.event.startsAt, slot + HOUR);
55
+ assert.notEqual(details?.event.eventSeriesId, null);
56
+ }),
57
+ test("rule 3: another calendar's copy with the same iCalUID is the same event", async (fixture) => {
58
+ const { store, calendar } = fixture;
59
+ const other = await second(fixture);
60
+ const [work] = await store.saveEntries(calendar, [single("w1", later, { icalUid: "uid-1@example.com" })], 2);
61
+ const [home] = await store.saveEntries(other, [single("p1", later, { icalUid: "uid-1@example.com" })], 3);
62
+ assert.equal(home?.eventId, work?.eventId);
63
+ assert.equal((await store.event(work?.eventId))?.entries.length, 2);
64
+ }),
65
+ test("rule 5: otherwise a new event of origin calendar; a slot cancelled before it was stored is none", async ({ store, calendar, }) => {
66
+ const [one, two, gone] = await store.saveEntries(calendar, [single("a", later), single("b", later), single("c", later, { status: "cancelled" })], 2);
67
+ assert.notEqual(one?.eventId, two?.eventId);
68
+ assert.equal(gone?.eventId, null);
69
+ assert.equal((await store.events({ from: later, to: later + HOUR })).length, 2);
70
+ }),
71
+ test("two candidates: no link and one review entry, kept through a second sync and an owner's decision", async (fixture) => {
72
+ const { store, calendar } = fixture;
73
+ const other = await second(fixture);
74
+ const third = await second(fixture, "third");
75
+ const [a] = await store.saveEntries(calendar, [single("a", later, { icalUid: "first" })], 2);
76
+ const [b] = await store.saveEntries(other, [single("b", later, { icalUid: "second" })], 2);
77
+ await store.saveEntries(calendar, [single("a", later, { icalUid: "shared" })], 3);
78
+ await store.saveEntries(other, [single("b", later, { icalUid: "shared" })], 3);
79
+ const [c] = await store.saveEntries(third, [single("c", later, { icalUid: "shared" })], 4);
80
+ assert.equal(c?.eventId, null);
81
+ await store.saveEntries(third, [single("c", later, { icalUid: "shared" })], 5);
82
+ const open = await store.reviews({ status: "open" });
83
+ assert.equal(open.length, 1);
84
+ assert.deepEqual(open[0]?.candidates, [a?.eventId, b?.eventId].sort((x, y) => x - y));
85
+ await store.resolveReview(open[0]?.id, { choose: b?.eventId }, 6);
86
+ await store.saveEntries(third, [single("c", later, { icalUid: "shared" })], 7);
87
+ const [after] = await store.saveEntries(third, [single("c", later, { icalUid: "shared" })], 8);
88
+ assert.equal(after?.eventId, b?.eventId);
89
+ assert.deepEqual((await store.reviews()).map(({ status }) => status), ["linked"]);
90
+ }),
91
+ test("the owner's unlink holds: no rule links the entry again", async ({ store, calendar }) => {
92
+ const [entry] = await store.saveEntries(calendar, [single("a", later)], 2);
93
+ await store.unlinkEntry(entry?.id, 3);
94
+ const [again] = await store.saveEntries(calendar, [single("a", later, { title: "Renamed" })], 4);
95
+ assert.equal(again?.eventId, null);
96
+ await store.linkEntry(entry?.id, entry?.eventId, 5);
97
+ const [linked] = await store.saveEntries(calendar, [single("a", later)], 6);
98
+ assert.equal(linked?.eventId, entry?.eventId);
99
+ }),
100
+ test("a deleted single event: past stays as history, future is cancelled", async ({ store, calendar }) => {
101
+ const [past, future] = await store.saveEntries(calendar, [single("p", earlier), single("f", later)], 2);
102
+ await store.saveEntries(calendar, [single("p", earlier, { status: "cancelled" }), single("f", later, { status: "cancelled" })], CONTRACT_NOW);
103
+ const happened = (await store.event(past?.eventId))?.event;
104
+ assert.equal(happened?.sourceDeletedAt, CONTRACT_NOW);
105
+ assert.notEqual(happened?.status, "cancelled");
106
+ assert.equal(happened?.title, "Example review");
107
+ const cancelled = (await store.event(future?.eventId))?.event;
108
+ assert.equal(cancelled?.status, "cancelled");
109
+ assert.equal(cancelled?.sourceDeletedAt, null);
110
+ }),
111
+ test("a whole series deleted: its entries cancel, past occurrences stay, future ones cancel", async ({ store, calendar, }) => {
112
+ const series = {
113
+ ...single("s", earlier - HOUR),
114
+ kind: "series",
115
+ icalUid: "series@example.com",
116
+ recurrence: ["RRULE:FREQ=DAILY"],
117
+ };
118
+ const occurrence = (at) => single(`s_${at}`, at, { kind: "occurrence", seriesExternalId: "s", originalStartsAt: at });
119
+ const [, past, future] = await store.saveEntries(calendar, [series, occurrence(earlier), occurrence(later)], 2);
120
+ await store.saveEntries(calendar, [{ ...series, status: "cancelled" }], CONTRACT_NOW);
121
+ const before = await store.event(past?.eventId);
122
+ assert.equal(before?.event.sourceDeletedAt, CONTRACT_NOW);
123
+ assert.equal(before?.entries[0]?.status, "cancelled");
124
+ const after = await store.event(future?.eventId);
125
+ assert.equal(after?.event.status, "cancelled");
126
+ }),
127
+ test("a full list marks the entries it did not return, plain occurrences only inside the window", async ({ store, calendar, }) => {
128
+ const outside = later + 30 * 24 * HOUR;
129
+ const [kept, gone, far] = await store.saveEntries(calendar, [
130
+ single("kept", later),
131
+ single("gone", later),
132
+ single("far", outside, { kind: "occurrence", seriesExternalId: "x", originalStartsAt: outside }),
133
+ ], 2);
134
+ await store.saveEntries(calendar, [single("kept", later)], 10);
135
+ assert.equal(await store.markUnseen(calendar, 10, { from: earlier, to: later + 24 * HOUR }, 11), 1);
136
+ const entries = new Map((await store.event(gone?.eventId))?.entries.map((entry) => [entry.externalId, entry]));
137
+ assert.equal(entries.get("gone")?.deletedAt, 11);
138
+ assert.notEqual(await store.event(gone?.eventId), null);
139
+ assert.equal((await store.event(kept?.eventId))?.entries[0]?.deletedAt, null);
140
+ assert.equal((await store.event(far?.eventId))?.entries[0]?.deletedAt, null);
141
+ }),
142
+ test("participants are kept by address; a room is listed and is no contact", async ({ store, calendar }) => {
143
+ const [entry] = await store.saveEntries(calendar, [
144
+ single("a", later, {
145
+ participants: [
146
+ { email: "Alice@Example.com", name: "Alice Example", response: "accepted" },
147
+ { email: "room-1@resource.example.com", name: "Room 1", resource: true },
148
+ ],
149
+ }),
150
+ ], 2);
151
+ const participants = (await store.event(entry?.eventId))?.participants ?? [];
152
+ assert.deepEqual(participants.map(({ email, resource, identityId }) => [email, resource, identityId === null]), [
153
+ ["alice@example.com", false, false],
154
+ ["room-1@resource.example.com", true, true],
155
+ ]);
156
+ }),
157
+ test("an entry that ends before it starts is refused", async ({ store, calendar }) => {
158
+ await assert.rejects(store.saveEntries(calendar, [single("bad", later, { endsAt: later - 1 })], 2));
159
+ }),
160
+ ];
161
+ };
@@ -1,4 +1,5 @@
1
1
  export { meetingStoreContract } from "./contract.js";
2
+ export { CONTRACT_NOW, type EventStoreFixture, eventStoreContract } from "./event-store-contract.js";
2
3
  export { type MeetingManagementContractFixture, meetingManagementContract } from "./management-contract.js";
3
4
  export { memoryMeetingStore } from "./memory.js";
4
5
  export { type RecordingSyncCheckpointFixture, recordingSyncCheckpointStoreContract, } from "./recording-sync-contract.js";
@@ -1,4 +1,5 @@
1
1
  export { meetingStoreContract } from "./contract.js";
2
+ export { CONTRACT_NOW, eventStoreContract } from "./event-store-contract.js";
2
3
  export { meetingManagementContract } from "./management-contract.js";
3
4
  export { memoryMeetingStore } from "./memory.js";
4
5
  export { recordingSyncCheckpointStoreContract, } from "./recording-sync-contract.js";
@@ -1,4 +1,3 @@
1
- import { MOVED_TO_MEMO } from "../cli/moved.js";
2
1
  export const longName = (flags) => flags.split(/[\s,|]+/).find((part) => part.startsWith("--")) ?? flags;
3
2
  const presence = (entry) => (typeof entry === "string" ? { in: entry } : entry);
4
3
  /** `true` the CLI must have it, `false` it must not, `undefined` it is planned and either passes. */
@@ -93,9 +92,6 @@ export const manifestProblems = (manifest) => {
93
92
  }
94
93
  optionEntries(path, row.options ?? {});
95
94
  }
96
- for (const path of Object.keys(manifest.commands))
97
- if (Object.keys(MOVED_TO_MEMO).some((moved) => path === moved || path.startsWith(`${moved} `)))
98
- problems.push(`${path}: moved to memo (src/cli/moved.ts), so a source tool has no row for it`);
99
95
  for (const [name, option] of Object.entries(manifest.options)) {
100
96
  if (!option.meaning)
101
97
  problems.push(`options ${name}: no meaning`);
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { FOLDER_PROVIDER, INTERNAL_NOTES, isKind, kindIn, kindOf, providerNamed, providers as registered, UNKNOWN_PROVIDER, } from "../domain/providers.js";
2
+ import { INTERNAL_NOTES, isDocumentAccountProvider, isFolderProvider, isKind, kindIn, kindOf, providerNamed, providers as registered, UNKNOWN_PROVIDER, } from "../domain/providers.js";
3
3
  import { isBotProvider } from "./query.js";
4
4
  /** Store-wide: every profile, tg, MAX and memo reads the same groups. */
5
5
  export const ACCOUNT_GROUPS = "accountGroups";
@@ -29,13 +29,29 @@ const describe = (accounts) => accounts.map(({ provider, account, name }) => `${
29
29
  const kindHere = (provider, running) => provider === running.provider ? kindOf(provider) : kindIn(provider);
30
30
  const heldOf = (stored, running) => [
31
31
  ...stored
32
- .filter(({ provider }) => kindIn(provider) !== "notes")
32
+ .filter(({ provider }) => kindIn(provider) !== "notes" && !isDocumentAccountProvider(provider))
33
33
  .map(({ provider, account, name }) => ({ provider, account, name, kind: kindHere(provider, running) })),
34
34
  { provider: "notes", account: INTERNAL_NOTES, name: null, kind: "notes" },
35
35
  ...stored
36
- .filter(({ provider }) => provider === FOLDER_PROVIDER)
37
- .map(({ id, name }) => ({ provider: "notes", account: String(id), name, kind: "notes" })),
36
+ .filter(({ provider }) => isFolderProvider(provider))
37
+ .map(({ id, name, provider }) => ({
38
+ provider: "notes",
39
+ account: String(id),
40
+ name,
41
+ kind: "notes",
42
+ folder: provider,
43
+ })),
44
+ ...stored
45
+ .filter(({ provider }) => isDocumentAccountProvider(provider))
46
+ .map(({ id, provider, account, name }) => ({
47
+ provider: "notes",
48
+ account: String(id),
49
+ name,
50
+ kind: "notes",
51
+ stored: { provider, account },
52
+ })),
38
53
  ];
54
+ const storedAs = (held, key) => held.filter(({ stored }) => stored?.provider === key.provider && (key.account === "" || stored.account === key.account));
39
55
  const registry = () => registered()
40
56
  .map(({ name, aliases }) => (aliases.length ? `${name} (${aliases.join(", ")})` : name))
41
57
  .join(", ");
@@ -44,7 +60,7 @@ const providerOf = (prefix, held, { running, profiles }) => {
44
60
  if (wanted === running.command?.toLowerCase())
45
61
  return running.provider;
46
62
  const providers = [...new Set(held.map(({ provider }) => provider))];
47
- if (providers.includes(wanted))
63
+ if (providers.includes(wanted) || isDocumentAccountProvider(wanted))
48
64
  return wanted;
49
65
  const named = providerNamed(wanted);
50
66
  if (named)
@@ -57,6 +73,11 @@ const providerOf = (prefix, held, { running, profiles }) => {
57
73
  const profilesOf = (provider, profiles) => Object.values(profiles).flatMap((named) => Object.entries(named).filter(([, key]) => key.provider === provider));
58
74
  const accountOf = (prefix, ref, held, context) => {
59
75
  const provider = providerOf(prefix, held, context);
76
+ if (isDocumentAccountProvider(provider) && !context.profiles[prefix.toLowerCase()]?.[ref]) {
77
+ const stored = storedAs(held, { provider, account: ref })[0];
78
+ if (stored)
79
+ return stored;
80
+ }
60
81
  const ofProvider = held.filter((one) => one.provider === provider);
61
82
  const byId = ofProvider.find(({ account }) => account === ref || account === ref.toLowerCase());
62
83
  if (byId)
@@ -67,12 +88,18 @@ const accountOf = (prefix, ref, held, context) => {
67
88
  kind: kindHere(key.provider, context.running),
68
89
  };
69
90
  const recorded = context.profiles[prefix.toLowerCase()]?.[ref];
70
- if (recorded?.provider === FOLDER_PROVIDER) {
91
+ if (recorded && isFolderProvider(recorded.provider)) {
71
92
  const folder = held.find((one) => one.provider === "notes" && one.name === recorded.account);
72
93
  if (folder)
73
94
  return folder;
74
95
  return fail(`no stored folder for ${prefix}:${ref}; sync that profile first`);
75
96
  }
97
+ if (recorded && isDocumentAccountProvider(recorded.provider)) {
98
+ const account = storedAs(held, recorded)[0];
99
+ if (account)
100
+ return account;
101
+ return fail(`no stored documents for ${prefix}:${ref}; sync that profile first`);
102
+ }
76
103
  if (recorded)
77
104
  return keyed(recorded);
78
105
  const byProfile = [
@@ -101,9 +128,14 @@ const accountsOfWord = (word, held, context) => {
101
128
  if (isKind(wanted))
102
129
  return held.filter(({ kind }) => kind === wanted);
103
130
  const recorded = context.profiles[wanted];
104
- if (recorded && Object.values(recorded).every((key) => key.provider === FOLDER_PROVIDER))
131
+ if (recorded &&
132
+ Object.values(recorded).every((key) => isFolderProvider(key.provider) || isDocumentAccountProvider(key.provider)))
105
133
  return Object.keys(recorded).map((profile) => accountOf(word, profile, held, context));
106
134
  const provider = providerOf(word, held, context);
135
+ if (isDocumentAccountProvider(provider))
136
+ return storedAs(held, { provider, account: "" });
137
+ if (isFolderProvider(provider))
138
+ return held.filter(({ folder }) => folder === provider);
107
139
  return held.filter((one) => one.provider === provider);
108
140
  };
109
141
  const keyOf = ({ provider, account }) => `${provider}\u0000${account}`;
@@ -3,7 +3,7 @@ export { type FloodDeadline, FloodMemory, type FloodState, FROZEN_HOLD_MS, flood
3
3
  export { type Asker, type GuardRequest, guardFor, type SendGuard, type SendGuardOptions, sendGuard, sharedJournal, } from "./guard.js";
4
4
  export { currentOperation, guardedWrite, type Operated } from "./guarded.js";
5
5
  export { type AccountAction, type ChatAction, type SendEntry, SendJournal, type SendKind, type SendOutcome, sendsPathFor, } from "./journal.js";
6
- export { assertStatsPermissionsCurrent, CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, PERMISSIONS, type Permission, type PermissionKey, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
6
+ export { CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, type PermissionKey, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
7
7
  export { type Recipient, RecipientList, recipientsPathFor } from "./recipients.js";
8
8
  export { newOperationId, newSendId } from "./send-id.js";
9
9
  export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
@@ -2,7 +2,7 @@ export { FloodMemory, FROZEN_HOLD_MS, floodPathFor, LIMITED_HOLD_MS, } from "./f
2
2
  export { guardFor, sendGuard, sharedJournal, } from "./guard.js";
3
3
  export { currentOperation, guardedWrite } from "./guarded.js";
4
4
  export { SendJournal, sendsPathFor, } from "./journal.js";
5
- export { assertStatsPermissionsCurrent, CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, PERMISSIONS, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
5
+ export { CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
6
6
  export { RecipientList, recipientsPathFor } from "./recipients.js";
7
7
  export { newOperationId, newSendId } from "./send-id.js";
8
8
  export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
@@ -1,13 +1,4 @@
1
1
  import type { AccountAction, ChatAction, SendKind } from "./journal.js";
2
- /**
3
- * What a profile may be allowed to do (max-cli `CLI-37`, `NEED-251`). One name per thing the owner would
4
- * recognise, so `sessions` — which logs the owner out of their phone — never rides along with
5
- * adding a contact. There is no `*`: "everything" is not setting `allow` at all, and a wildcard
6
- * would switch on `delete` without anyone naming it.
7
- */
8
- export declare const PERMISSIONS: readonly ["send", "forward", "reaction", "edit", "pin", "read", "delete", "groups", "contacts", "profile", "folders", "sessions"];
9
- export type Permission = (typeof PERMISSIONS)[number];
10
- export declare const permissionFor: (kind: SendKind, action?: string) => Permission;
11
2
  /** What a profile may do with one command path — the standard's "Permissions". */
12
3
  export declare const LEVELS: readonly ["deny", "readonly", "ask", "allow"];
13
4
  export type Level = (typeof LEVELS)[number];
@@ -32,8 +23,6 @@ export declare const isBotApi: (path: readonly string[]) => boolean;
32
23
  export declare const botApiAllowed: (levels: Readonly<Record<PermissionKey, Level>>) => boolean;
33
24
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
34
25
  export declare const RESOURCES: readonly ["messages", "drafts", "reactions", "polls", "topics", "chats", "contacts", "account", "bot", "conversations", "tags", "mentions", "search", "searches", "tasks", "replies", "attachments", "stats", "store", "metadata", "mcp", "server", "models", "schedule", "doctor", "images"];
35
- /** The command path an old `allow` word stood for, as one key — what an MCP tool's `permission` is checked against. */
36
- export declare const keyOfWord: (word: Permission) => PermissionKey;
37
26
  /**
38
27
  * Layers come nearest first. A key a nearer layer sets hides that key and every key under it in the
39
28
  * farther layers, so a profile's `messages: readonly` is not undone by `messages.delete: allow` in
@@ -67,8 +56,6 @@ export declare const writeOfKey: (key: PermissionKey) => {
67
56
  };
68
57
  /** Every key a write is checked against that a command path may not spell out. */
69
58
  export declare const WRITE_KEYS: readonly PermissionKey[];
70
- /** Statistics and searches moved; a deny under an old path would stop applying unseen, so the command refuses. */
71
- export declare const assertStatsPermissionsCurrent: (path: readonly string[], permissions: Readonly<Record<string, Level>>) => void;
72
59
  export declare const readKeysForCommand: (path: readonly string[]) => PermissionKey[];
73
60
  /**
74
61
  * The key a command path is checked against: `null` for housekeeping, which no level stops, and
@@ -1,66 +1,4 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { canonicalPermissionKey, canonicalPermissions } from "./permission-aliases.js";
3
- /**
4
- * What a profile may be allowed to do (max-cli `CLI-37`, `NEED-251`). One name per thing the owner would
5
- * recognise, so `sessions` — which logs the owner out of their phone — never rides along with
6
- * adding a contact. There is no `*`: "everything" is not setting `allow` at all, and a wildcard
7
- * would switch on `delete` without anyone naming it.
8
- */
9
- export const PERMISSIONS = [
10
- "send",
11
- "forward",
12
- "reaction",
13
- "edit",
14
- "pin",
15
- "read",
16
- "delete",
17
- "groups",
18
- "contacts",
19
- "profile",
20
- "folders",
21
- "sessions",
22
- ];
23
- const ACCOUNT = {
24
- "contact-add": "contacts",
25
- "contact-remove": "contacts",
26
- "contact-import": "contacts",
27
- "contact-rename": "contacts",
28
- "contact-block": "contacts",
29
- "contact-unblock": "contacts",
30
- profile: "profile",
31
- "folder-create": "folders",
32
- "folder-update": "folders",
33
- "folder-delete": "folders",
34
- "folder-order": "folders",
35
- "folder-join": "folders",
36
- "sessions-end": "sessions",
37
- "chat-mute": "profile",
38
- "chat-unmute": "profile",
39
- "chat-archive": "profile",
40
- "chat-unarchive": "profile",
41
- "chat-pin": "profile",
42
- "chat-unpin": "profile",
43
- "chat-mark-unread": "profile",
44
- "message-star": "profile",
45
- "message-unstar": "profile",
46
- privacy: "profile",
47
- };
48
- export const permissionFor = (kind, action) => {
49
- switch (kind) {
50
- case "message":
51
- return "send";
52
- case "chat":
53
- return "groups";
54
- case "account": {
55
- const permission = ACCOUNT[action];
56
- if (!permission)
57
- throw new Error(`an account change without a known action: ${action}`);
58
- return permission;
59
- }
60
- default:
61
- return kind;
62
- }
63
- };
64
2
  /** What a profile may do with one command path — the standard's "Permissions". */
65
3
  export const LEVELS = ["deny", "readonly", "ask", "allow"];
66
4
  /** The deletions nobody gets back: the word that skips their question is `--allow-dangerous`, never `--yes`. */
@@ -148,47 +86,6 @@ export const RESOURCES = [
148
86
  "doctor",
149
87
  "images",
150
88
  ];
151
- const OLD_WORDS = {
152
- send: ["messages.send", "polls.create", "chats.start"],
153
- forward: ["messages.forward"],
154
- reaction: ["reactions", "polls.vote", "messages.press", "chats.app"],
155
- edit: ["messages.edit", "polls.close"],
156
- pin: ["messages.pin", "messages.unpin"],
157
- read: ["chats.mark-read"],
158
- delete: ["messages.delete", "chats.delete", "chats.clear"],
159
- groups: [
160
- "chats.create",
161
- "chats.update",
162
- "chats.join",
163
- "chats.leave",
164
- "chats.members",
165
- "chats.admins",
166
- "chats.links",
167
- "chats.requests",
168
- "chats.moderate",
169
- "topics.enable",
170
- "topics.create",
171
- "topics.edit",
172
- "topics.delete",
173
- ],
174
- contacts: ["contacts"],
175
- profile: ["account.update", "account.privacy.set", "chats.mute", "messages.star", "messages.unstar"],
176
- folders: ["chats.folders"],
177
- sessions: ["account.sessions.end"],
178
- };
179
- /** The command path an old `allow` word stood for, as one key — what an MCP tool's `permission` is checked against. */
180
- export const keyOfWord = (word) => OLD_WORDS[word][0];
181
- /**
182
- * A bot's old words meant its own writes: `profile` was its command menu, and `read` was taking
183
- * updates, which every level but `deny` allows now.
184
- */
185
- const botKeysOf = (word) => {
186
- if (word === "profile")
187
- return ["bot.commands"];
188
- if (word === "read")
189
- return [];
190
- return OLD_WORDS[word].map((key) => `bot.${key}`);
191
- };
192
89
  /**
193
90
  * Layers come nearest first. A key a nearer layer sets hides that key and every key under it in the
194
91
  * farther layers, so a profile's `messages: readonly` is not undone by `messages.delete: allow` in
@@ -199,7 +96,7 @@ export const layerPermissions = (layers) => {
199
96
  const sources = {};
200
97
  for (const [from, layer] of layers) {
201
98
  const nearer = Object.keys(levels);
202
- for (const [key, level] of Object.entries(canonicalPermissions(layer ?? {}))) {
99
+ for (const [key, level] of Object.entries(layer ?? {})) {
203
100
  if (nearer.some((named) => key === named || key.startsWith(`${named}.`)))
204
101
  continue;
205
102
  levels[key] = level;
@@ -224,11 +121,6 @@ const nearest = (levels, key) => {
224
121
  }
225
122
  return undefined;
226
123
  };
227
- /**
228
- * `chats unmute` was checked as `chats.mute` before it had its own key; a config that names only the old key
229
- * keeps meaning it, so a `chats.mute: readonly` written then still stops an unmute.
230
- */
231
- const FORMER_KEYS = { "chats.unmute": "chats.mute" };
232
124
  /**
233
125
  * The most specific key the owner set wins. A built-in default (`DEFAULT_PERMISSIONS`) only ever
234
126
  * tightens: against a broader key of the owner's, the stricter of the two holds — so
@@ -236,16 +128,11 @@ const FORMER_KEYS = { "chats.unmute": "chats.mute" };
236
128
  * A path nothing names is allowed.
237
129
  */
238
130
  export const levelFor = (permissions, key, defaults = DEFAULT_PERMISSIONS) => {
239
- permissions = canonicalPermissions(permissions);
240
- key = canonicalPermissionKey(key);
241
131
  const found = profileLevel(permissions, key, defaults);
242
132
  const ceiling = permissions[CEILING_KEY];
243
133
  return ceiling && ORDER[ceiling] < ORDER[found.level] ? { level: ceiling, key: CEILING_KEY } : found;
244
134
  };
245
135
  const profileLevel = (permissions, key, defaults) => {
246
- const former = FORMER_KEYS[key];
247
- if (former !== undefined && permissions[key] === undefined)
248
- return profileLevel(permissions, former, defaults);
249
136
  const own = nearest(permissions, key);
250
137
  const builtIn = nearest(defaults, key);
251
138
  if (!builtIn || (own && own.depth >= builtIn.depth))
@@ -254,7 +141,15 @@ const profileLevel = (permissions, key, defaults) => {
254
141
  return { level: builtIn.level, key: builtIn.key };
255
142
  return { level: own.level, key: own.key };
256
143
  };
257
- const CHAT_KEYS = { settings: "chats.update" };
144
+ const CHAT_KEYS = {
145
+ settings: "chats.update",
146
+ "requests.accept": "chats.requests.confirm",
147
+ "requests.decline": "chats.requests.reject",
148
+ "link.create": "chats.links.create",
149
+ "link.reset": "chats.links.reset",
150
+ "link.revoke": "chats.links.remove",
151
+ "link.update": "chats.links.update",
152
+ };
258
153
  const ACCOUNT_KEYS = {
259
154
  "contact-add": "contacts.add",
260
155
  "contact-remove": "contacts.remove",
@@ -292,7 +187,7 @@ const KIND_KEYS = {
292
187
  /** The command path a guarded write belongs to, when the caller did not name it. */
293
188
  export const keyForWrite = (kind, action) => {
294
189
  if (kind === "chat")
295
- return canonicalPermissionKey(CHAT_KEYS[action ?? ""] ?? `chats.${action ?? "update"}`);
190
+ return CHAT_KEYS[action] ?? `chats.${action ?? "update"}`;
296
191
  if (kind === "account") {
297
192
  const key = ACCOUNT_KEYS[action];
298
193
  if (!key)
@@ -303,7 +198,6 @@ export const keyForWrite = (kind, action) => {
303
198
  };
304
199
  /** `keyForWrite` read backwards, as far as a command's key says it: an unpin is a `pin`, a chat change names its action. */
305
200
  export const writeOfKey = (key) => {
306
- key = canonicalPermissionKey(key);
307
201
  const account = Object.entries(ACCOUNT_KEYS).find(([, named]) => named === key)?.[0];
308
202
  if (account)
309
203
  return { kind: "account", action: account };
@@ -399,8 +293,7 @@ export const WRITE_KEYS = [
399
293
  ...Object.values(KIND_KEYS),
400
294
  ...Object.values(ACCOUNT_KEYS),
401
295
  ...Object.values(CHAT_KEYS),
402
- ...PERMISSIONS.flatMap((word) => [...OLD_WORDS[word], ...botKeysOf(word)]),
403
- ].map(canonicalPermissionKey)),
296
+ ]),
404
297
  ];
405
298
  /** Commands that look after the tool and its files and never show a message: no level stops them. */
406
299
  const HOUSEKEEPING = new Set([
@@ -430,21 +323,6 @@ const BOT_HOUSEKEEPING = new Set(["auth", "list", "sends", "recipients", "mcp"])
430
323
  const STORE_MAINTENANCE = new Set(["info", "check", "copies"]);
431
324
  /** Commands outside `messages` that print what people wrote, so `deny messages` reaches them too. */
432
325
  const SHOW_MESSAGES = new Set(["inbox", "review", "watch", "serve", "store", "conversations"]);
433
- /** Before every search moved under `search` (2026-10-09). */
434
- const OLD_SEARCH_KEY = /^(bot\.)?(messages|conversations|topics)\.search(?:\.|$)/;
435
- /** Statistics and searches moved; a deny under an old path would stop applying unseen, so the command refuses. */
436
- export const assertStatsPermissionsCurrent = (path, permissions) => {
437
- if (path[0] === "bot") {
438
- assertStatsPermissionsCurrent(path.slice(1), permissions);
439
- return;
440
- }
441
- if (path[0] === "search" && Object.keys(permissions).some((key) => OLD_SEARCH_KEY.test(key)))
442
- throw new CliError("configuration_error", "search permission paths have moved — rename messages.search, conversations.search and topics.search to search.messages, search.conversations and search.topics", { retryable: false });
443
- if (path[0] !== "stats")
444
- return;
445
- if (Object.keys(permissions).some((key) => /^(bot\.)?(messages|chats|tasks)\.stats(?:\.|$)/.test(key)))
446
- throw new CliError("configuration_error", "statistics permission paths have moved — rename messages.stats, chats.stats and tasks.stats to stats.messages.show, stats.chats.show and stats.tasks.show", { retryable: false });
447
- };
448
326
  export const readKeysForCommand = (path) => {
449
327
  if (path[0] === "bot")
450
328
  return readKeysForCommand(path.slice(1)).map((key) => `bot.${key}`);
@@ -488,7 +366,6 @@ export const readKeysForCommand = (path) => {
488
366
  export const keyForCommand = (path) => {
489
367
  if (path.length === 0)
490
368
  return null;
491
- path = canonicalPermissionKey(path.join(".")).split(".");
492
369
  const [top, next] = path;
493
370
  if (top === "bot") {
494
371
  if (next === undefined || BOT_HOUSEKEEPING.has(next))
@@ -500,7 +377,7 @@ export const keyForCommand = (path) => {
500
377
  return path.join(".");
501
378
  if (top === undefined || HOUSEKEEPING.has(top) || (top === "search" && next === "runs"))
502
379
  return null;
503
- // Picture models moved from `models image` (0.270.0); a model is housekeeping wherever it is listed.
380
+ // A model is housekeeping wherever it is listed.
504
381
  if (top === "images" && next === "models")
505
382
  return null;
506
383
  if (top === "attachments" && (next === "check" || next === "jobs"))
@@ -513,7 +390,7 @@ export const keyForCommand = (path) => {
513
390
  if (top === "conversations" && next === "links")
514
391
  return path[2] === "rebuild" ? "conversations.links.rebuild" : "conversations.links";
515
392
  // Vectors are written to the local store only, and the text never leaves the machine (phase 5 E10).
516
- if ((top === "conversations" && next === "embeddings") ||
393
+ if ((top === "conversations" && (next === "embeddings" || next === "embed")) ||
517
394
  (top === "images" && next === "embed") ||
518
395
  (top === "conversations" && next === "rebuild"))
519
396
  return "conversations.embeddings";
@@ -526,8 +403,6 @@ export const keyForCommand = (path) => {
526
403
  return next === "rebuild" || next === "confirm" || next === "reject" ? `mentions.${next}` : "messages";
527
404
  if (top === "store" && (next === "gaps" || next === "jobs"))
528
405
  return path.join(".");
529
- if (top === "messages" && next === "sync")
530
- return "messages";
531
406
  if (SHOW_MESSAGES.has(top) || (top === "contacts" && (next === "context" || next === "prepare" || next === "events")))
532
407
  return "messages";
533
408
  if (RESOURCES.includes(top))
@@ -544,12 +419,10 @@ export const permissionOverrides = (entries = []) => {
544
419
  throw new CliError("validation_error", "--permission takes a command path and level, e.g. messages.send=allow (deny, readonly, ask, allow)");
545
420
  levels[key] = level;
546
421
  }
547
- return canonicalPermissions(levels);
422
+ return levels;
548
423
  };
549
424
  /** No startup flag grants access: an override may narrow the profile's level, never lift it or a key under it. */
550
425
  export const assertOverridesTighten = (overrides, profile) => {
551
- overrides = canonicalPermissions(overrides);
552
- profile = canonicalPermissions(profile);
553
426
  for (const [key, level] of Object.entries(overrides)) {
554
427
  const under = Object.keys(profile).filter((named) => named.startsWith(`${key}.`));
555
428
  const lifted = [key, ...under].find((named) => ORDER[level] > ORDER[levelFor(profile, named).level]);
@@ -5,6 +5,7 @@ import { agenda } from "../entities/agenda.js";
5
5
  import { AGENDA_COLUMNS, inboxKey, queryFilter, } from "../store/store.js";
6
6
  import { CHAT_KINDS } from "./chats.js";
7
7
  import { taskAccount } from "./task-rules.js";
8
+ import { taskTagFilters } from "./task-tag-filters.js";
8
9
  const DAY = 86_400_000;
9
10
  const CLAIMS = ["none", "mine", "others", "any"];
10
11
  const TASK_FIELDS = new Set(["tag", "category", "claimed"]);
@@ -46,8 +47,9 @@ export const agendaService = (deps, now = Date.now) => ({
46
47
  throw error;
47
48
  });
48
49
  const open = await find();
50
+ const tags = await taskTagFilters(store, parts.tasks);
49
51
  const listed = parts.tasks
50
- ? await find(queryFilter(parts.tasks, taskColumns(holder, at), { command: "tasks agenda" }))
52
+ ? await find(queryFilter(parts.tasks, taskColumns(holder, at, tags), { command: "tasks agenda" }))
51
53
  : open;
52
54
  const tasks = listed
53
55
  .map((task) => ({ task, reasons: reasonsOf(task, at, dueWithin) }))
@@ -148,11 +150,9 @@ const split = (query) => {
148
150
  };
149
151
  return { tasks: of("tasks"), chats: of("chats") };
150
152
  };
151
- const TAGGED = "EXISTS (SELECT 1 FROM taggings g JOIN tags n ON n.id = g.tag_id " +
152
- "WHERE g.taggable_type = 'task' AND g.taggable_id = t.id AND n.name = ?)";
153
153
  /** A claim that lapsed holds nothing, as `tasks claim` reads it. */
154
- const taskColumns = (holder, at) => ({
155
- tag: { where: (node) => ({ sql: TAGGED, params: [node.value.trim().toLowerCase()] }) },
154
+ const taskColumns = (holder, at, tags) => ({
155
+ tag: { where: (node) => tags.get(node.value) ?? { sql: "0", params: [] } },
156
156
  category: { type: "kw", sql: "t.category" },
157
157
  claimed: {
158
158
  where: (node) => {