@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
@@ -12,11 +12,11 @@ const folded = (name) => {
12
12
  /** Includes both label kinds and aliases: a narrower preview must not hide a collision. */
13
13
  export const tagCreationReview = ({ database }, name, kind) => {
14
14
  const metadata = database
15
- .prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(name AS BLOB))),0) AS bytes FROM tags")
15
+ .prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(name AS BLOB))),0) AS bytes FROM canonical_tags")
16
16
  .get();
17
17
  const aliasMetadata = database
18
18
  .prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(a.name AS BLOB))),0) AS bytes FROM aliases a " +
19
- "JOIN tags t ON t.id=a.aliasable_id WHERE a.aliasable_type='tag' AND a.account_id IS NULL")
19
+ "JOIN canonical_tags t ON t.id=a.aliasable_id WHERE a.aliasable_type='tag' AND a.account_id IS NULL")
20
20
  .get();
21
21
  if (Number(metadata?.count ?? 0) > 5000 ||
22
22
  Number(aliasMetadata?.count ?? 0) > 100000 ||
@@ -24,9 +24,11 @@ export const tagCreationReview = ({ database }, name, kind) => {
24
24
  throw new CliError("validation_error", "label creation review exceeds its metadata budget", {
25
25
  reason: "tag-review.over-budget",
26
26
  });
27
- const rows = database.prepare("SELECT id,name,kind FROM tags ORDER BY id").all();
27
+ const rows = database
28
+ .prepare("SELECT t.id,t.name,t.kind,r.canonical_id,r.name AS canonical_name FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id ORDER BY t.id")
29
+ .all();
28
30
  const aliases = database
29
- .prepare("SELECT a.id,a.aliasable_id,a.name FROM aliases a JOIN tags t ON t.id=a.aliasable_id " +
31
+ .prepare("SELECT a.id,a.aliasable_id,a.name FROM aliases a JOIN canonical_tags t ON t.id=a.aliasable_id " +
30
32
  "WHERE a.aliasable_type='tag' AND a.account_id IS NULL ORDER BY a.id")
31
33
  .all();
32
34
  const names = new Map();
@@ -36,14 +38,21 @@ export const tagCreationReview = ({ database }, name, kind) => {
36
38
  values.push(String(alias.name));
37
39
  names.set(id, values);
38
40
  }
39
- const candidates = rows
41
+ const scored = rows
40
42
  .map((row) => ({
41
- id: String(row.id),
42
- name: String(row.name),
43
+ id: String(row.canonical_id),
44
+ name: String(row.canonical_name),
43
45
  kind: row.kind,
44
46
  similarity: Math.max(...[String(row.name), ...(names.get(Number(row.id)) ?? [])].map((value) => folded(value) === null ? 0 : vocabularySimilarity(name, value))),
45
47
  }))
46
- .filter((entry) => entry.similarity >= 0.65)
48
+ .filter((entry) => entry.similarity >= 0.65);
49
+ const grouped = new Map();
50
+ for (const entry of scored) {
51
+ const held = grouped.get(entry.id);
52
+ if (!held || entry.similarity > held.similarity)
53
+ grouped.set(entry.id, entry);
54
+ }
55
+ const candidates = [...grouped.values()]
47
56
  .sort((a, b) => b.similarity - a.similarity || Number(a.id) - Number(b.id))
48
57
  .slice(0, 10);
49
58
  return {
@@ -0,0 +1,5 @@
1
+ import { type TagRedirectSnapshot } from "../../domain/tag-redirects.js";
2
+ import type { CacheDatabase } from "../driver.js";
3
+ export declare const tagCanonicalId: (database: CacheDatabase, id: number) => number;
4
+ export declare const tagMemberIds: (database: CacheDatabase, id: number) => number[];
5
+ export declare const tagRedirectSnapshot: (database: CacheDatabase) => TagRedirectSnapshot;
@@ -0,0 +1,33 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { createTagRedirectIndex } from "../../domain/tag-redirects.js";
3
+ export const tagCanonicalId = (database, id) => {
4
+ const row = database.prepare("SELECT canonical_id FROM resolved_tags WHERE id=?").get(id);
5
+ if (!row)
6
+ throw new CliError("not_found", "the selected tag is unavailable");
7
+ return Number(row.canonical_id);
8
+ };
9
+ export const tagMemberIds = (database, id) => {
10
+ const canonical = tagCanonicalId(database, id);
11
+ return database
12
+ .prepare("SELECT ? AS id UNION ALL SELECT source_id AS id FROM tag_redirects WHERE survivor_id=? ORDER BY id")
13
+ .all(canonical, canonical)
14
+ .map((row) => Number(row.id));
15
+ };
16
+ export const tagRedirectSnapshot = (database) => {
17
+ const tags = database.prepare("SELECT id FROM canonical_tags WHERE kind='tag' ORDER BY id LIMIT 4097").all();
18
+ const redirects = database
19
+ .prepare("SELECT source_id,survivor_id FROM tag_redirects ORDER BY source_id LIMIT 4097")
20
+ .all();
21
+ if (tags.length > 4096 || redirects.length > 4096)
22
+ throw new CliError("validation_error", "tag consolidation exceeds the complete graph budget of 4096 rows");
23
+ const snapshot = {
24
+ complete: true,
25
+ tags: tags.map((row) => ({ id: String(row.id), kind: "tag" })),
26
+ redirects: redirects.map((row) => ({ sourceId: String(row.source_id), survivorId: String(row.survivor_id) })),
27
+ };
28
+ createTagRedirectIndex(snapshot);
29
+ const sources = new Set(snapshot.redirects.map((row) => row.sourceId));
30
+ if (snapshot.redirects.some((row) => sources.has(row.survivorId)))
31
+ throw new CliError("configuration_error", "tag redirects must point directly to a canonical tag");
32
+ return snapshot;
33
+ };
@@ -75,7 +75,7 @@ export declare const createTag: (context: StoreContext, name: string, kind: TagK
75
75
  export declare const ensureTag: (context: StoreContext, name: string) => TagRow;
76
76
  /** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
77
77
  export declare const addTags: (context: StoreContext, thing: Thing, tags: string[], source?: TaggingSource, author?: Actor) => string[];
78
- /** The tags it had, in the order given. A chat's keyword claim stays apart from the owner's own tag. */
78
+ /** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
79
79
  export declare const removeTags: ({ database }: StoreContext, thing: Thing, tags: string[], source?: "manual" | "auto") => string[];
80
80
  export interface Label {
81
81
  tag: string;
@@ -83,6 +83,8 @@ export interface Label {
83
83
  origin: "file" | "owner";
84
84
  }
85
85
  /** A thing's tags by name, with whether only a file states each. */
86
+ /** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
87
+ export declare const liveTagging: (alias: string) => string;
86
88
  export declare const labelsOf: (database: CacheDatabase, thing: Thing, tag?: string) => Label[];
87
89
  /**
88
90
  * Marks one topic as the thing's main one, or clears it with `null`: the topic is tagged on it if it was
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { formatLocator } from "../../domain/locator.js";
3
3
  import { normalizeVocabularyName } from "../../domain/vocabulary.js";
4
4
  import { guardTagCreation } from "./tag-creation.js";
5
+ import { tagMemberIds } from "./tag-resolution.js";
5
6
  import { inboxKey, taskAccountOf } from "./task-keys.js";
6
7
  import { emailOf, emailThreadOf, taskRowOf } from "./things.js";
7
8
  import { toIso } from "./values.js";
@@ -53,7 +54,9 @@ export const targetThing = ({ database }, key, target) => {
53
54
  return { type: TAGGABLE[target.type], id: Number(found.id) };
54
55
  };
55
56
  export const tagNamed = (database, name) => {
56
- const row = database.prepare("SELECT id, name, kind FROM tags WHERE name = ?").get(name);
57
+ const row = database
58
+ .prepare("SELECT r.canonical_id AS id,r.name,r.kind FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE t.name=?")
59
+ .get(name);
57
60
  return row ? { id: Number(row.id), name: String(row.name), kind: row.kind } : undefined;
58
61
  };
59
62
  /** An explicit alias reuses its target; similarity never chooses a canonical label. */
@@ -62,17 +65,18 @@ export const canonicalTagNamed = (database, name) => {
62
65
  // createTag stores the name as given, so "Projects/Acme" must be found again by its folded form.
63
66
  const foldedNamed = () => {
64
67
  const rows = database
65
- .prepare("SELECT id, name, kind FROM tags WHERE lower(name) = lower(?) LIMIT 3")
68
+ .prepare("SELECT r.canonical_id AS id,r.name,r.kind,t.name AS original_name FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE lower(t.name)=lower(?) LIMIT 4097")
66
69
  .all(normalized)
67
- .filter((row) => normalizeVocabularyName(String(row.name)) === normalized);
68
- if (rows.length > 1)
70
+ .filter((row) => normalizeVocabularyName(String(row.original_name)) === normalized);
71
+ const unique = [...new Map(rows.map((row) => [Number(row.id), row])).values()];
72
+ if (unique.length > 1)
69
73
  throw new CliError("configuration_error", "the label name has conflicting canonical targets");
70
- const row = rows[0];
74
+ const row = unique[0];
71
75
  return row ? { id: Number(row.id), name: String(row.name), kind: row.kind } : undefined;
72
76
  };
73
77
  const direct = tagNamed(database, normalized) ?? foldedNamed();
74
78
  const aliases = database
75
- .prepare("SELECT DISTINCT t.id,t.name,t.kind FROM aliases a JOIN tags t ON t.id=a.aliasable_id " +
79
+ .prepare("SELECT DISTINCT t.canonical_id AS id,t.name,t.kind FROM aliases a JOIN resolved_tags t ON t.id=a.aliasable_id " +
76
80
  "WHERE a.aliasable_type='tag' AND a.account_id IS NULL AND a.name_folded=? LIMIT 2")
77
81
  .all(normalized);
78
82
  if (aliases.length > 1 || (direct && aliases.some((row) => Number(row.id) !== direct.id)))
@@ -97,7 +101,9 @@ export const createTag = (context, name, kind, by, options = {}) => {
97
101
  throw new CliError("validation_error", held.kind === kind ? `the ${kind} "${name}" exists` : `"${name}" is a ${held.kind}; one name is never both`, { reason: held.kind === kind ? "tag_exists" : "tag_name_clash" });
98
102
  guardTagCreation(context, name, kind, by, options);
99
103
  const at = now();
100
- database.prepare("INSERT INTO tags (name, kind, created_at, updated_at) VALUES (?, ?, ?, ?)").run(name, kind, at, at);
104
+ database
105
+ .prepare("INSERT INTO canonical_tags (name, kind, created_at, updated_at) VALUES (?, ?, ?, ?)")
106
+ .run(name, kind, at, at);
101
107
  return tagNamed(database, name);
102
108
  };
103
109
  /** The tag of that name, made as a plain tag when no tag or topic holds the name yet. */
@@ -113,51 +119,64 @@ export const addTags = (context, thing, tags, source = "owner", author) => {
113
119
  const at = now();
114
120
  return tags.filter((name) => {
115
121
  const tag = ensureTag(context, name);
122
+ const held = database
123
+ .prepare("SELECT source,author_type,author_id FROM taggings WHERE tag_id IN (SELECT ? UNION ALL SELECT source_id FROM tag_redirects WHERE survivor_id=?) AND taggable_type=? AND taggable_id=?")
124
+ .all(tag.id, tag.id, thing.type, thing.id);
125
+ if (held.some((row) => row.source === source && row.author_type === (author?.type ?? null) && row.author_id === (author?.id ?? null)))
126
+ return false;
116
127
  const added = insert.run(tag.id, thing.type, thing.id, source, author?.type ?? null, author?.id ?? null, at, at).changes > 0;
117
128
  if (!added && source === "owner")
118
129
  claim.run(at, tag.id, thing.type, thing.id);
119
- return added;
130
+ return added && held.length === 0;
120
131
  });
121
132
  };
122
- const sourceOf = (database, thing, tagId) => database
123
- .prepare("SELECT source FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
124
- .get(tagId, thing.type, thing.id)?.source;
125
- /** The tags it had, in the order given. A chat's keyword claim stays apart from the owner's own tag. */
133
+ /** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
126
134
  export const removeTags = ({ database }, thing, tags, source) => {
127
135
  const remove = database.prepare("DELETE FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?");
128
136
  return tags.filter((name) => {
129
137
  const tag = canonicalTagNamed(database, name);
130
138
  if (!tag)
131
139
  return false;
132
- const held = sourceOf(database, thing, tag.id);
133
- const automatic = thing.type === "chat" &&
134
- database.prepare("SELECT 1 AS held FROM auto_tag_claims WHERE chat_id=? AND tag_id=?").get(thing.id, tag.id);
135
- if (source === "manual") {
136
- if (held !== "owner")
137
- return false;
138
- if (automatic)
140
+ let changed = false;
141
+ for (const id of tagMemberIds(database, tag.id)) {
142
+ const held = database
143
+ .prepare("SELECT source FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
144
+ .get(id, thing.type, thing.id);
145
+ const automatic = thing.type === "chat" &&
139
146
  database
140
- .prepare("UPDATE taggings SET source='auto' WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
141
- .run(tag.id, thing.type, thing.id);
147
+ .prepare("SELECT 1 AS held FROM canonical_auto_tag_claims WHERE chat_id=? AND tag_id=?")
148
+ .get(thing.id, id);
149
+ if (source === "manual") {
150
+ if (held?.source !== "owner")
151
+ continue;
152
+ if (automatic)
153
+ database
154
+ .prepare("UPDATE taggings SET source='auto' WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
155
+ .run(id, thing.type, thing.id);
156
+ else
157
+ remove.run(id, thing.type, thing.id);
158
+ changed = true;
159
+ continue;
160
+ }
161
+ if (automatic) {
162
+ database.prepare("DELETE FROM canonical_auto_tag_claims WHERE chat_id=? AND tag_id=?").run(thing.id, id);
163
+ changed = true;
164
+ }
165
+ if (source === "auto") {
166
+ if (held?.source === "auto")
167
+ changed = remove.run(id, thing.type, thing.id).changes > 0 || changed;
168
+ }
142
169
  else
143
- remove.run(tag.id, thing.type, thing.id);
144
- return true;
145
- }
146
- if (thing.type === "chat")
147
- database.prepare("DELETE FROM auto_tag_claims WHERE chat_id=? AND tag_id=?").run(thing.id, tag.id);
148
- if (source === "auto") {
149
- if (!automatic)
150
- return false;
151
- if (held !== "owner")
152
- remove.run(tag.id, thing.type, thing.id);
153
- return true;
170
+ changed = remove.run(id, thing.type, thing.id).changes > 0 || changed;
154
171
  }
155
- return remove.run(tag.id, thing.type, thing.id).changes > 0;
172
+ return changed;
156
173
  });
157
174
  };
158
175
  /** A thing's tags by name, with whether only a file states each. */
176
+ /** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
177
+ export const liveTagging = (alias) => `NOT (${alias}.taggable_type = 'document' AND EXISTS (SELECT 1 FROM documents dt WHERE dt.id = ${alias}.taggable_id AND dt.deleted_at IS NOT NULL))`;
159
178
  export const labelsOf = (database, thing, tag) => database
160
- .prepare(`SELECT t.name, g.source FROM taggings g JOIN tags t ON t.id = g.tag_id WHERE g.taggable_type=? AND g.taggable_id=?${tag === undefined ? "" : " AND t.name = ?"} ORDER BY t.name`)
179
+ .prepare(`SELECT t.name,CASE WHEN min(g.source)='file' AND max(g.source)='file' THEN 'file' ELSE 'owner' END AS source FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE g.taggable_type=? AND g.taggable_id=?${tag === undefined ? "" : " AND t.name = ?"} GROUP BY t.canonical_id ORDER BY t.name`)
161
180
  .all(thing.type, thing.id, ...(tag === undefined ? [] : [tag]))
162
181
  .map((row) => ({ tag: String(row.name), origin: row.source === "file" ? "file" : "owner" }));
163
182
  /**
@@ -181,45 +200,46 @@ export const setMainTopic = (context, thing, topic) => {
181
200
  .run(at, tag.id, thing.type, thing.id);
182
201
  };
183
202
  export const tagsOf = ({ database }, key, filter) => {
184
- const tagged = filter.tag === undefined ? "" : " AND t.name=?";
203
+ const selectedTag = filter.tag === undefined ? undefined : (canonicalTagNamed(database, filter.tag)?.id ?? -1);
204
+ const tagged = selectedTag === undefined ? "" : " AND t.canonical_id=(SELECT canonical_id FROM resolved_tags WHERE id=?)";
185
205
  const parts = [
186
206
  {
187
207
  type: "chat",
188
208
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, c.external_id AS chat_id, c.title AS chat_title, " +
189
- "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, NULL AS task_id " +
190
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN chats c ON c.id=g.taggable_id " +
209
+ "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
210
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN chats c ON c.id=g.taggable_id " +
191
211
  "JOIN accounts a ON a.id=c.account_id WHERE g.taggable_type='chat' AND a.provider=? AND a.external_id=?",
192
212
  params: [key.provider, key.account],
193
213
  },
194
214
  {
195
215
  type: "contact",
196
216
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, NULL AS chat_id, NULL AS chat_title, " +
197
- "i.external_id AS person_id, i.name AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, NULL AS task_id " +
198
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN identities i ON i.id=g.taggable_id " +
217
+ "i.external_id AS person_id, i.name AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
218
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN identities i ON i.id=g.taggable_id " +
199
219
  "WHERE g.taggable_type='identity' AND i.provider=?",
200
220
  params: [key.provider],
201
221
  },
202
222
  {
203
223
  type: "message",
204
224
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, c.external_id AS chat_id, c.title AS chat_title, " +
205
- "NULL AS person_id, NULL AS name, m.external_id AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, NULL AS task_id " +
206
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN messages m ON m.id=g.taggable_id JOIN chats c ON c.id=m.chat_id " +
225
+ "NULL AS person_id, NULL AS name, m.external_id AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
226
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN messages m ON m.id=g.taggable_id JOIN chats c ON c.id=m.chat_id " +
207
227
  "JOIN accounts a ON a.id=c.account_id WHERE g.taggable_type='message' AND a.provider=? AND a.external_id=?",
208
228
  params: [key.provider, key.account],
209
229
  },
210
230
  {
211
231
  type: "chat",
212
232
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, th.external_id AS chat_id, th.subject AS chat_title, " +
213
- "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, NULL AS task_id " +
214
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN email_threads th ON th.id=g.taggable_id " +
233
+ "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
234
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN email_threads th ON th.id=g.taggable_id " +
215
235
  "JOIN accounts a ON a.id=th.account_id WHERE g.taggable_type='email_thread' AND a.provider=? AND a.external_id=?",
216
236
  params: [key.provider, key.account],
217
237
  },
218
238
  {
219
239
  type: "message",
220
240
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, th.external_id AS chat_id, th.subject AS chat_title, " +
221
- "NULL AS person_id, NULL AS name, e.external_id AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, NULL AS task_id " +
222
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN emails e ON e.id=g.taggable_id " +
241
+ "NULL AS person_id, NULL AS name, e.external_id AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
242
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN emails e ON e.id=g.taggable_id " +
223
243
  "JOIN email_threads th ON th.id=e.email_thread_id JOIN accounts a ON a.id=e.account_id " +
224
244
  "WHERE g.taggable_type='email' AND e.deleted_at IS NULL AND a.provider=? AND a.external_id=?",
225
245
  params: [key.provider, key.account],
@@ -227,9 +247,9 @@ export const tagsOf = ({ database }, key, filter) => {
227
247
  {
228
248
  type: "task",
229
249
  sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, NULL AS chat_id, k.title AS chat_title, " +
230
- "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.id AS tag_id, " +
250
+ "NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, " +
231
251
  "COALESCE(k.package_id, k.key) AS task_id " +
232
- "FROM taggings g JOIN tags t ON t.id=g.tag_id JOIN tasks k ON k.id=g.taggable_id JOIN projects p ON p.id=k.project_id " +
252
+ "FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN tasks k ON k.id=g.taggable_id JOIN projects p ON p.id=k.project_id " +
233
253
  "WHERE g.taggable_type='task' AND k.deleted_at IS NULL AND p.key=?",
234
254
  params: [inboxKey(`${key.provider}:${key.account}`)],
235
255
  },
@@ -243,13 +263,13 @@ export const tagsOf = ({ database }, key, filter) => {
243
263
  : [{ sql: filter.source === "auto" ? "x.auto = 1" : "(x.auto = 0 OR x.source = 'owner')", params: [] }]),
244
264
  ];
245
265
  const rows = database
246
- .prepare("SELECT * FROM (SELECT u.*, CASE u.type WHEN 'identity' THEN 'contact' WHEN 'email_thread' THEN 'chat' " +
266
+ .prepare("SELECT * FROM (SELECT tag,type,min(created_at) AS created_at,chat_id,chat_title,person_id,name,message_id,CASE WHEN max(source='owner') THEN 'owner' ELSE min(source) END AS source,target_id,tag_id,task_id,tag_type,max(auto) AS auto FROM (SELECT u.*, CASE u.type WHEN 'identity' THEN 'contact' WHEN 'email_thread' THEN 'chat' " +
247
267
  "WHEN 'email' THEN 'message' ELSE u.type END AS tag_type, (u.type = 'chat' AND EXISTS (SELECT 1 FROM " +
248
- `auto_tag_claims ac WHERE ac.chat_id = u.target_id AND ac.tag_id = u.tag_id)) AS auto FROM (${union}) u) x ` +
268
+ `canonical_auto_tag_claims ac JOIN resolved_tags rt ON rt.id=ac.tag_id WHERE ac.chat_id = u.target_id AND rt.canonical_id = u.tag_id)) AS auto FROM (${union}) u) GROUP BY tag,type,target_id) x ` +
249
269
  `WHERE ${where.length === 0 ? "1" : where.map(({ sql }) => `(${sql})`).join(" AND ")} ` +
250
270
  "ORDER BY x.tag, x.type, x.created_at, x.chat_id, x.person_id, x.message_id" +
251
271
  (filter.window ? " LIMIT ? OFFSET ?" : ""))
252
- .all(...chosen.flatMap(({ params }) => (filter.tag === undefined ? params : [...params, filter.tag])), ...where.flatMap(({ params }) => params), ...(filter.window ? [filter.window.limit, filter.window.offset] : []));
272
+ .all(...chosen.flatMap(({ params }) => (selectedTag === undefined ? params : [...params, selectedTag])), ...where.flatMap(({ params }) => params), ...(filter.window ? [filter.window.limit, filter.window.offset] : []));
253
273
  const result = rows.map((row) => {
254
274
  const type = TAG_TYPE[String(row.type)];
255
275
  const chatId = row.chat_id == null ? undefined : String(row.chat_id);
@@ -23,6 +23,8 @@ export interface StoreTaskStore extends Omit<Required<TaskStore>, "reviewDuplica
23
23
  unlistedCategories(): Promise<UnlistedCategory[]>;
24
24
  /** What the task package calls a task — its id, or a key like `IN1A2B3C4D-3` — as the row's `tasks.id`. */
25
25
  rowOf(id: string): number | undefined;
26
+ /** A canonical/alias-aware label predicate for the task query's `t` row. */
27
+ tagWhere(name: string): Promise<Where>;
26
28
  /** One account's tasks a list query keeps, oldest first, each with its title and last change. */
27
29
  query(account: string, where: Where | undefined, window: {
28
30
  limit: number;
@@ -4,6 +4,7 @@ import { taskCategoriesReader } from "../shared-config.js";
4
4
  import { actorOfOrigin, originOfActor } from "./actors.js";
5
5
  import { folded } from "./folded.js";
6
6
  import { linkKind } from "./link-kinds.js";
7
+ import { canonicalTagNamed } from "./tags.js";
7
8
  import { taskSimilarityOver } from "./task-candidates.js";
8
9
  import { savedTaskCategories, saveTaskCategories, storeTaskCategories, unlistedTaskCategories, } from "./task-category-settings.js";
9
10
  import { CLEAR_SUGGESTION, closeSuggestionOf, SUGGESTION_COLUMNS } from "./task-close.js";
@@ -11,7 +12,7 @@ import { taskCloseOver } from "./task-close-store.js";
11
12
  import { inboxKey, typedKey } from "./task-keys.js";
12
13
  import { taskPairsOver } from "./task-pairs.js";
13
14
  import { taskRowOf, thingOf } from "./things.js";
14
- import { write } from "./transaction.js";
15
+ import { read, write } from "./transaction.js";
15
16
  import { recordTaskEvent, workTasksOver } from "./work-tasks.js";
16
17
  export { inboxKey, linkInbox, taskAccountOf } from "./task-keys.js";
17
18
  export { taskIdOf, taskRowOf } from "./things.js";
@@ -95,6 +96,10 @@ export const taskStoreOver = (context) => {
95
96
  categoriesFile: () => context.sharedConfig,
96
97
  saveCategories: async (categories) => write(database, () => saveTaskCategories(database, categories, now())),
97
98
  unlistedCategories: async () => unlistedTaskCategories(database, listOf()),
99
+ tagWhere: async (name) => read(database, () => ({
100
+ sql: "EXISTS (SELECT 1 FROM taggings g JOIN resolved_tags r ON r.id=g.tag_id WHERE g.taggable_type='task' AND g.taggable_id=t.id AND r.canonical_id=(SELECT canonical_id FROM resolved_tags WHERE id=?))",
101
+ params: [canonicalTagNamed(database, name)?.id ?? -1],
102
+ })),
98
103
  query: async (account, where, { limit, offset }) => {
99
104
  const project = inbox(account, false);
100
105
  if (project === undefined)
@@ -1,4 +1,5 @@
1
1
  import { formatLocator } from "../../domain/locator.js";
2
+ import { filesAccountSql } from "../../domain/providers.js";
2
3
  import { canonicalReference, formatReference, parseReference } from "../../domain/references.js";
3
4
  import { canonicalMeetingReference, formatMeetingReference, parseMeetingReference } from "../../meetings/index.js";
4
5
  import { ENTITY_TABLES, isEntityType } from "./entity-types.js";
@@ -72,7 +73,7 @@ export const thingOf = (database, reference) => {
72
73
  const id = storeId(parsed.id);
73
74
  return id === undefined
74
75
  ? undefined
75
- : found("account", database.prepare("SELECT id FROM accounts WHERE id = ? AND provider = 'folder'").get(id));
76
+ : found("account", database.prepare(`SELECT id FROM accounts WHERE id = ? AND ${filesAccountSql()}`).get(id));
76
77
  }
77
78
  case "task": {
78
79
  const id = taskRowOf(database, parsed.id);
@@ -8,6 +8,7 @@ export interface VocabularyTarget {
8
8
  export interface VocabularyAlias {
9
9
  id: string;
10
10
  name: string;
11
+ retained?: true;
11
12
  }
12
13
  export interface VocabularyEntry extends VocabularyTarget {
13
14
  name: string;
@@ -4,6 +4,7 @@ import { normalizeTag } from "../../domain/tags.js";
4
4
  import { normalizeVocabularyName, vocabularySimilarity } from "../../domain/vocabulary.js";
5
5
  import { tagConsolidationStoreOver } from "./tag-consolidation.js";
6
6
  import { tagCreationReview } from "./tag-creation.js";
7
+ import { liveTagging } from "./tags.js";
7
8
  import { read, write } from "./transaction.js";
8
9
  const kinds = ["project", "tag", "topic"];
9
10
  const cancelled = (signal) => {
@@ -40,7 +41,7 @@ export const vocabularyStoreOver = (context) => {
40
41
  const projects = options.kind === undefined || options.kind === "project";
41
42
  const tags = options.kind !== "project";
42
43
  const whereProjects = "account_id IS NULL AND deleted_at IS NULL AND (? IS NULL OR scope=?)";
43
- const whereTags = "(? IS NULL OR kind=?)";
44
+ const whereTags = "(? IS NULL OR kind=?) AND id=canonical_id";
44
45
  const tagKind = options.kind === "project" ? null : (options.kind ?? null);
45
46
  const metadata = [];
46
47
  if (projects)
@@ -49,7 +50,7 @@ export const vocabularyStoreOver = (context) => {
49
50
  .all(options.scope ?? null, options.scope ?? null, maxEntries + 1));
50
51
  if (tags)
51
52
  metadata.push(...database
52
- .prepare(`SELECT kind,id,length(CAST(name AS BLOB)) AS bytes FROM tags WHERE ${whereTags} LIMIT ?`)
53
+ .prepare(`SELECT kind,id,length(CAST(name AS BLOB)) AS bytes FROM resolved_tags WHERE ${whereTags} LIMIT ?`)
53
54
  .all(tagKind, tagKind, maxEntries + 1));
54
55
  if (metadata.length > maxEntries)
55
56
  throw new CliError("validation_error", "vocabulary exceeds the selected entry budget", {
@@ -57,7 +58,7 @@ export const vocabularyStoreOver = (context) => {
57
58
  });
58
59
  const aliasSelection = "a.account_id IS NULL AND ((? AND a.aliasable_type='project' AND EXISTS " +
59
60
  "(SELECT 1 FROM projects p WHERE p.id=a.aliasable_id AND p.account_id IS NULL AND p.deleted_at IS NULL AND (? IS NULL OR p.scope=?))) " +
60
- "OR (? AND a.aliasable_type='tag' AND EXISTS (SELECT 1 FROM tags t WHERE t.id=a.aliasable_id AND (? IS NULL OR t.kind=?))))";
61
+ "OR (? AND a.aliasable_type='tag' AND EXISTS (SELECT 1 FROM resolved_tags t WHERE t.id=a.aliasable_id AND (? IS NULL OR t.kind=?))))";
61
62
  const aliasParams = [
62
63
  Number(projects),
63
64
  options.scope ?? null,
@@ -71,11 +72,26 @@ export const vocabularyStoreOver = (context) => {
71
72
  .get(...aliasParams);
72
73
  if (Number(aliases?.count ?? 0) > maxEntries * 20)
73
74
  throw new CliError("validation_error", "vocabulary aliases exceed the selected entry budget");
74
- const bytes = metadata.reduce((total, row) => total + Number(row.bytes), 0) + Number(aliases?.bytes ?? 0);
75
+ const retainedWhere = "t.id<>r.canonical_id AND (? IS NULL OR t.kind=?)";
76
+ const retainedMetadata = tags
77
+ ? database
78
+ .prepare(`SELECT count(*) AS count,coalesce(sum(length(CAST(t.name AS BLOB))),0) AS bytes FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE ${retainedWhere}`)
79
+ .get(tagKind, tagKind)
80
+ : undefined;
81
+ if (Number(retainedMetadata?.count ?? 0) > maxEntries)
82
+ throw new CliError("validation_error", "retained tag names exceed the selected entry budget");
83
+ const bytes = metadata.reduce((total, row) => total + Number(row.bytes), 0) +
84
+ Number(aliases?.bytes ?? 0) +
85
+ Number(retainedMetadata?.bytes ?? 0);
75
86
  if (bytes > maxReadBytes)
76
87
  throw new CliError("validation_error", "vocabulary exceeds the selected input byte budget", {
77
88
  reason: "vocabulary.over-byte-budget",
78
89
  });
90
+ const retained = tags
91
+ ? database
92
+ .prepare(`SELECT t.id,t.name,r.canonical_id FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE ${retainedWhere} ORDER BY t.id LIMIT ?`)
93
+ .all(tagKind, tagKind, maxEntries + 1)
94
+ : [];
79
95
  const entries = [];
80
96
  if (projects)
81
97
  entries.push(...database
@@ -92,7 +108,7 @@ export const vocabularyStoreOver = (context) => {
92
108
  })));
93
109
  if (tags)
94
110
  entries.push(...database
95
- .prepare(`SELECT t.id,t.name,t.kind,(SELECT count(*) FROM taggings g WHERE g.tag_id=t.id)+(SELECT count(*) FROM links l WHERE l.to_type='tag' AND l.to_id=t.id AND l.kind='labelled') AS uses FROM tags t WHERE ${whereTags} ORDER BY id`)
111
+ .prepare(`SELECT t.id,t.name,t.kind,(SELECT count(*) FROM (SELECT DISTINCT g.taggable_type,g.taggable_id FROM taggings g JOIN resolved_tags r ON r.id=g.tag_id WHERE r.canonical_id=t.id AND ${liveTagging("g")}))+(SELECT count(*) FROM (SELECT DISTINCT l.from_type,l.from_id,l.anchor FROM links l JOIN resolved_tags r ON r.id=l.to_id WHERE l.to_type='tag' AND r.canonical_id=t.id AND l.kind='labelled')) AS uses FROM resolved_tags t WHERE ${whereTags} ORDER BY id`)
96
112
  .all(tagKind, tagKind)
97
113
  .map((row) => ({
98
114
  kind: row.kind,
@@ -105,11 +121,15 @@ export const vocabularyStoreOver = (context) => {
105
121
  })));
106
122
  const byTarget = new Map(entries.map((entry) => [`${entry.kind === "project" ? "project" : "tag"}:${entry.id}`, entry]));
107
123
  for (const row of database
108
- .prepare(`SELECT a.id,a.aliasable_type,a.aliasable_id,a.name FROM aliases a WHERE ${aliasSelection} ORDER BY a.id`)
124
+ .prepare(`SELECT a.id,a.aliasable_type,CASE WHEN a.aliasable_type='tag' THEN (SELECT canonical_id FROM resolved_tags WHERE id=a.aliasable_id) ELSE a.aliasable_id END AS aliasable_id,a.name FROM aliases a WHERE ${aliasSelection} ORDER BY a.id`)
109
125
  .all(...aliasParams))
110
126
  byTarget
111
127
  .get(`${row.aliasable_type}:${row.aliasable_id}`)
112
128
  ?.aliases.push({ id: String(row.id), name: String(row.name) });
129
+ for (const row of retained)
130
+ byTarget
131
+ .get(`tag:${row.canonical_id}`)
132
+ ?.aliases.push({ id: `tag:${row.id}`, name: String(row.name), retained: true });
113
133
  cancelled(options.signal);
114
134
  return {
115
135
  entries,
@@ -253,13 +273,15 @@ export const vocabularyStoreOver = (context) => {
253
273
  removeVocabularyAlias: async (target, aliasId, options = {}) => write(database, () => {
254
274
  const current = writable(options);
255
275
  const entry = targetEntry(current, target);
276
+ if (entry.aliases.some((alias) => alias.id === aliasId && alias.retained))
277
+ throw new CliError("validation_error", "aliasId names a retained tag name; its original record cannot be removed as an alias");
256
278
  idOf(aliasId);
257
279
  const held = entry.aliases.find((alias) => alias.id === aliasId);
258
280
  if (!held)
259
281
  return { target: { kind: entry.kind, id: entry.id }, alias: null, changed: false };
260
282
  database
261
- .prepare("DELETE FROM aliases WHERE id=? AND aliasable_type=? AND aliasable_id=? AND account_id IS NULL")
262
- .run(idOf(aliasId), target.kind === "project" ? "project" : "tag", idOf(target.id));
283
+ .prepare("DELETE FROM aliases WHERE id=? AND aliasable_type=? AND (aliasable_id=? OR (aliasable_type='tag' AND aliasable_id IN (SELECT id FROM resolved_tags WHERE canonical_id=?))) AND account_id IS NULL")
284
+ .run(idOf(aliasId), target.kind === "project" ? "project" : "tag", idOf(target.id), idOf(target.id));
263
285
  cancelled(options.signal);
264
286
  return { target: { kind: entry.kind, id: entry.id }, alias: held, changed: true };
265
287
  }),
@@ -1,4 +1,5 @@
1
1
  import type { ContextEvidenceReader, ContextIndex, ContextRecordReader } from "../context/evidence.js";
2
+ import type { EventStore } from "../meetings/index.js";
2
3
  import { type AccountStore } from "./account-store.js";
3
4
  import { type AccountsStore } from "./accounts.js";
4
5
  import { type AttachmentsStore } from "./attachments.js";
@@ -20,6 +21,7 @@ import { type BotUpdateStore } from "./sqlite/bot-updates.js";
20
21
  import { type CatalogStore } from "./sqlite/catalog.js";
21
22
  import { type MailStore } from "./sqlite/emails.js";
22
23
  import { type FactsStore } from "./sqlite/facts.js";
24
+ import { type FilesStore } from "./sqlite/files.js";
23
25
  import { type InvolvementStore } from "./sqlite/involvements.js";
24
26
  import { type KnowledgeStore } from "./sqlite/knowledge.js";
25
27
  import { type MeetingVectors } from "./sqlite/meeting-vectors.js";
@@ -79,6 +81,10 @@ export interface MessageStore {
79
81
  /** Notes, the links between anything and anything, and the owner's organisations and projects. */
80
82
  readonly notes: NotesStore;
81
83
  readonly catalog: CatalogStore;
84
+ /** Where each document is kept: its files, their folders and paths. */
85
+ readonly files: FilesStore;
86
+ /** Calendars and their entries, the source records of events: the `EventStore` a calendar CLI writes through. */
87
+ readonly calendar: EventStore;
82
88
  /** Choices that hold until replaced, each with its evidence. */
83
89
  readonly meetingFindings: MeetingFindingsStore;
84
90
  /** Cited facts and follow-ups about one project from stored messages, mail and documents, for review. */
@@ -15,11 +15,13 @@ import * as accounts from "./sqlite/accounts.js";
15
15
  import { agentActionsStoreOver } from "./sqlite/agent-actions.js";
16
16
  import * as attachmentTexts from "./sqlite/attachment-texts.js";
17
17
  import { botUpdateStoreOver } from "./sqlite/bot-updates.js";
18
+ import { eventStoreOver } from "./sqlite/calendar.js";
18
19
  import { catalogStoreOver } from "./sqlite/catalog.js";
19
20
  import { contextEvidenceOver, contextIndexOver, contextRecordsOver } from "./sqlite/context-evidence.js";
20
21
  import { mailStoreOver } from "./sqlite/emails.js";
21
22
  import { evidenceFindingsStoreOver } from "./sqlite/evidence-findings.js";
22
23
  import { factsStoreOver } from "./sqlite/facts.js";
24
+ import { filesStoreOver } from "./sqlite/files.js";
23
25
  import { involvementStoreOver } from "./sqlite/involvements.js";
24
26
  import { knowledgeStoreOver } from "./sqlite/knowledge.js";
25
27
  import { logPruner } from "./sqlite/log-pruning.js";
@@ -96,6 +98,8 @@ const storeOver = (context) => {
96
98
  const knowledge = once(() => knowledgeStoreOver(context));
97
99
  const notes = once(() => ({ ...notesStoreOver(context), ...noteSearchOver(context) }));
98
100
  const catalog = once(() => catalogStoreOver(context));
101
+ const files = once(() => filesStoreOver(context));
102
+ const calendar = once(() => eventStoreOver(context));
99
103
  const meetingFindings = once(() => meetingFindingsStoreOver(context));
100
104
  const evidenceFindings = once(() => evidenceFindingsStoreOver(context));
101
105
  const facts = once(() => factsStoreOver(context));
@@ -183,6 +187,12 @@ const storeOver = (context) => {
183
187
  get catalog() {
184
188
  return catalog();
185
189
  },
190
+ get files() {
191
+ return files();
192
+ },
193
+ get calendar() {
194
+ return calendar();
195
+ },
186
196
  get meetingFindings() {
187
197
  return meetingFindings();
188
198
  },
@@ -1,5 +1,5 @@
1
1
  import * as tagQueries from "./sqlite/tags.js";
2
- import { write } from "./sqlite/transaction.js";
2
+ import { read, write } from "./sqlite/transaction.js";
3
3
  export const tagsStoreOver = (context) => {
4
4
  const { database } = context;
5
5
  return {
@@ -17,6 +17,6 @@ export const tagsStoreOver = (context) => {
17
17
  });
18
18
  return removed;
19
19
  },
20
- tags: async (key, filter = {}) => tagQueries.tagsOf(context, key, filter),
20
+ tags: async (key, filter = {}) => read(database, () => tagQueries.tagsOf(context, key, filter)),
21
21
  };
22
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.419.0",
3
+ "version": "0.421.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -214,6 +214,7 @@
214
214
  "@types/pngjs": "6.0.5",
215
215
  "@vitest/coverage-v8": "^5.0.2",
216
216
  "@wirecat/cli-base": "0.53.0",
217
+ "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
217
218
  "commander": "15.0.0",
218
219
  "cspell": "10.3.6",
219
220
  "drizzle-kit": "1.0.0-rc.4",