synomem 0.6.10 → 0.7.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 (53) hide show
  1. package/dist/backend.d.ts.map +1 -1
  2. package/dist/backend.js +1 -0
  3. package/dist/backend.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +139 -43
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +22 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +164 -3
  10. package/dist/client.js.map +1 -1
  11. package/dist/errors.d.ts +1 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +2 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/import.d.ts +83 -0
  16. package/dist/import.d.ts.map +1 -1
  17. package/dist/mcp/index.d.ts.map +1 -1
  18. package/dist/mcp/index.js +110 -2
  19. package/dist/mcp/index.js.map +1 -1
  20. package/dist/ports/repository.d.ts +10 -1
  21. package/dist/ports/repository.d.ts.map +1 -1
  22. package/dist/projections.d.ts.map +1 -1
  23. package/dist/projections.js +6 -0
  24. package/dist/projections.js.map +1 -1
  25. package/dist/remote.d.ts +22 -1
  26. package/dist/remote.d.ts.map +1 -1
  27. package/dist/remote.js +18 -4
  28. package/dist/remote.js.map +1 -1
  29. package/dist/schemas.d.ts +130 -0
  30. package/dist/schemas.d.ts.map +1 -1
  31. package/dist/schemas.js +77 -0
  32. package/dist/schemas.js.map +1 -1
  33. package/dist/service.d.ts +14 -1
  34. package/dist/service.d.ts.map +1 -1
  35. package/dist/storage.d.ts +12 -1
  36. package/dist/storage.d.ts.map +1 -1
  37. package/dist/storage.js +136 -19
  38. package/dist/storage.js.map +1 -1
  39. package/dist/types.d.ts +80 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/backend.ts +1 -0
  43. package/src/cli.ts +305 -91
  44. package/src/client.ts +184 -2
  45. package/src/errors.ts +2 -0
  46. package/src/mcp/index.ts +140 -1
  47. package/src/ports/repository.ts +8 -0
  48. package/src/projections.ts +6 -0
  49. package/src/remote.ts +65 -4
  50. package/src/schemas.ts +85 -0
  51. package/src/service.ts +18 -0
  52. package/src/storage.ts +167 -13
  53. package/src/types.ts +90 -2
package/src/storage.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  readJsonFile,
24
24
  } from './fs-utils.js';
25
25
  import { dueInstant } from './projections.js';
26
- import { actorSchema, eventSchema, profileSchema } from './schemas.js';
26
+ import { actorSchema, eventSchema, profileSchema, topicProfileSchema } from './schemas.js';
27
27
  import type {
28
28
  ActorIdentity,
29
29
  SynomemConfig,
@@ -39,6 +39,7 @@ import type {
39
39
  ItemChange,
40
40
  ItemListInput,
41
41
  ItemSummary,
42
+ Topic,
42
43
  RecordKind,
43
44
  KudosListInput,
44
45
  KudosSummary,
@@ -62,6 +63,7 @@ interface CurrentRow {
62
63
  actor_display_name: string | null;
63
64
  title: string;
64
65
  tags_json: string;
66
+ topic_ids_json: string;
65
67
  visibility: KudosSummary['visibility'];
66
68
  status: KudosSummary['status'];
67
69
  revocation_status: KudosSummary['revocationStatus'];
@@ -80,6 +82,7 @@ interface ItemRow {
80
82
  actor_display_name: string | null;
81
83
  title: string;
82
84
  tags_json: string;
85
+ topic_ids_json: string;
83
86
  visibility: ItemSummary['visibility'];
84
87
  status: string;
85
88
  recipient_agent_id: string | null;
@@ -99,6 +102,16 @@ interface ProfileRow {
99
102
  profile_json: string;
100
103
  }
101
104
 
105
+ interface TopicRow {
106
+ id: string;
107
+ display_name: string;
108
+ status: 'active' | 'archived';
109
+ created_at: string;
110
+ }
111
+
112
+ /** The schema version this file writes and expects. */
113
+ const SUPPORTED_SCHEMA_VERSION = 8;
114
+
102
115
  const migrationV1 = `
103
116
  CREATE TABLE IF NOT EXISTS schema_migrations (
104
117
  version INTEGER PRIMARY KEY,
@@ -181,6 +194,7 @@ CREATE TABLE kudos_current (
181
194
  actor_display_name TEXT,
182
195
  title TEXT NOT NULL,
183
196
  tags_json TEXT NOT NULL CHECK (json_valid(tags_json)),
197
+ topic_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(topic_ids_json)),
184
198
  visibility TEXT NOT NULL,
185
199
  status TEXT NOT NULL,
186
200
  revocation_status TEXT NOT NULL,
@@ -270,6 +284,25 @@ CREATE UNIQUE INDEX agents_handle ON agents(handle);
270
284
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
271
285
  `;
272
286
 
287
+ const migrationV8 = `
288
+ -- Topics: a controlled, reusable subject a record can be filed under,
289
+ -- distinct from a free-text tag (§ Topic in types.ts). One canonical display
290
+ -- name and a set of case-folded aliases, so a topic can be renamed without
291
+ -- retagging every record that already carries it.
292
+ CREATE TABLE topics (
293
+ id TEXT PRIMARY KEY,
294
+ display_name TEXT NOT NULL,
295
+ status TEXT NOT NULL DEFAULT 'active',
296
+ created_at TEXT NOT NULL,
297
+ updated_at TEXT NOT NULL
298
+ ) STRICT;
299
+ CREATE TABLE topic_aliases (
300
+ alias TEXT PRIMARY KEY,
301
+ topic_id TEXT NOT NULL REFERENCES topics(id)
302
+ ) STRICT;
303
+ CREATE INDEX topic_aliases_topic ON topic_aliases(topic_id);
304
+ `;
305
+
273
306
  const migrationV3 = `
274
307
  DROP TRIGGER IF EXISTS events_append_only_update;
275
308
  DROP TRIGGER IF EXISTS events_append_only_delete;
@@ -317,6 +350,7 @@ CREATE TABLE items_current (
317
350
  actor_display_name TEXT,
318
351
  title TEXT NOT NULL,
319
352
  tags_json TEXT NOT NULL CHECK (json_valid(tags_json)),
353
+ topic_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(topic_ids_json)),
320
354
  visibility TEXT NOT NULL,
321
355
  status TEXT NOT NULL,
322
356
  recipient_agent_id TEXT,
@@ -379,6 +413,7 @@ function itemSummaryFromRow(row: ItemRow): ItemSummary {
379
413
  },
380
414
  title: row.title,
381
415
  tags: JSON.parse(row.tags_json) as string[],
416
+ topicIds: JSON.parse(row.topic_ids_json) as string[],
382
417
  visibility: row.visibility,
383
418
  status: row.status,
384
419
  ...(row.recipient_agent_id ? { recipientAgentId: row.recipient_agent_id } : {}),
@@ -419,6 +454,7 @@ function summaryFromRow(row: CurrentRow): KudosSummary {
419
454
  },
420
455
  title: row.title,
421
456
  tags: JSON.parse(row.tags_json) as string[],
457
+ topicIds: JSON.parse(row.topic_ids_json) as string[],
422
458
  visibility: row.visibility,
423
459
  status: row.status,
424
460
  revocationStatus: row.revocation_status,
@@ -534,7 +570,7 @@ export class SynomemStorage implements SynomemRepository {
534
570
  const version = Number(
535
571
  (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
536
572
  );
537
- if (version > 7) {
573
+ if (version > SUPPORTED_SCHEMA_VERSION) {
538
574
  throw new SynomemError(
539
575
  'UNSUPPORTED_SCHEMA',
540
576
  `Database schema version ${version} is newer than this package supports.`,
@@ -626,14 +662,38 @@ export class SynomemStorage implements SynomemRepository {
626
662
  db.exec('PRAGMA user_version = 7');
627
663
  });
628
664
  }
665
+ const afterV7 = Number(
666
+ (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
667
+ );
668
+ if (afterV7 === 7) {
669
+ this.transactionSync(() => {
670
+ db.exec(migrationV8);
671
+ // A database bootstrapped fresh today already gets topic_ids_json
672
+ // from V1/V3's own CREATE TABLE text, so this only fires for a
673
+ // database that reached v7 before those columns existed — adding a
674
+ // column SQLite has no "IF NOT EXISTS" form for.
675
+ for (const table of ['kudos_current', 'items_current']) {
676
+ const hasColumn = (
677
+ db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
678
+ ).some((column) => column.name === 'topic_ids_json');
679
+ if (!hasColumn) {
680
+ db.exec(`ALTER TABLE ${table} ADD COLUMN topic_ids_json TEXT NOT NULL DEFAULT '[]'`);
681
+ }
682
+ }
683
+ db.prepare(
684
+ 'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
685
+ ).run(8, new Date().toISOString());
686
+ db.exec('PRAGMA user_version = 8');
687
+ });
688
+ }
629
689
  }
630
690
 
631
691
  private assertSchemaSupported(): void {
632
692
  const version = Number(
633
693
  (this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
634
694
  );
635
- if (version !== 7) {
636
- if (version >= 1 && version <= 6) {
695
+ if (version !== SUPPORTED_SCHEMA_VERSION) {
696
+ if (version >= 1 && version < SUPPORTED_SCHEMA_VERSION) {
637
697
  throw new SynomemError(
638
698
  'UNSUPPORTED_SCHEMA',
639
699
  `Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
@@ -641,7 +701,7 @@ export class SynomemStorage implements SynomemRepository {
641
701
  }
642
702
  throw new SynomemError(
643
703
  'UNSUPPORTED_SCHEMA',
644
- `Expected database schema version 5; found ${version}.`,
704
+ `Expected database schema version ${SUPPORTED_SCHEMA_VERSION}; found ${version}.`,
645
705
  );
646
706
  }
647
707
  }
@@ -772,9 +832,9 @@ export class SynomemStorage implements SynomemRepository {
772
832
  .prepare(
773
833
  `INSERT INTO kudos_current(
774
834
  kudos_id, given_sequence, created_at, recipient_agent_id, recipient_display_name,
775
- actor_kind, actor_id, actor_display_name, title, tags_json, visibility,
835
+ actor_kind, actor_id, actor_display_name, title, tags_json, topic_ids_json, visibility,
776
836
  status, revocation_status, updated_sequence
777
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unacknowledged', 'active', ?)`,
837
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unacknowledged', 'active', ?)`,
778
838
  )
779
839
  .run(
780
840
  event.id,
@@ -787,6 +847,7 @@ export class SynomemStorage implements SynomemRepository {
787
847
  event.actor.displayName ?? null,
788
848
  event.title,
789
849
  JSON.stringify(event.tags ?? []),
850
+ JSON.stringify(event.topicIds ?? []),
790
851
  event.visibility,
791
852
  sequence,
792
853
  );
@@ -810,6 +871,7 @@ export class SynomemStorage implements SynomemRepository {
810
871
  kind: RecordKind;
811
872
  title: string;
812
873
  tags?: string[];
874
+ topicIds?: string[];
813
875
  visibility: ItemSummary['visibility'];
814
876
  status: string;
815
877
  recipientAgentId?: string;
@@ -824,10 +886,10 @@ export class SynomemStorage implements SynomemRepository {
824
886
  .prepare(
825
887
  `INSERT INTO items_current(
826
888
  item_id, kind, created_sequence, updated_sequence, created_at, updated_at,
827
- actor_kind, actor_id, actor_display_name, title, tags_json, visibility, status,
889
+ actor_kind, actor_id, actor_display_name, title, tags_json, topic_ids_json, visibility, status,
828
890
  recipient_agent_id, recipient_display_name, owner_agent_id, owner_display_name,
829
891
  assignee_agent_id, assignee_display_name, due_at
830
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
892
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
831
893
  )
832
894
  .run(
833
895
  event.aggregateId,
@@ -841,6 +903,7 @@ export class SynomemStorage implements SynomemRepository {
841
903
  event.actor.displayName ?? null,
842
904
  values.title,
843
905
  JSON.stringify(values.tags ?? []),
906
+ JSON.stringify(values.topicIds ?? []),
844
907
  values.visibility,
845
908
  values.status,
846
909
  values.recipientAgentId ?? null,
@@ -865,6 +928,7 @@ export class SynomemStorage implements SynomemRepository {
865
928
  kind: 'kudos',
866
929
  title: event.title,
867
930
  tags: event.tags,
931
+ topicIds: event.topicIds,
868
932
  visibility: event.visibility,
869
933
  status: 'unacknowledged',
870
934
  recipientAgentId: event.recipientAgentId,
@@ -877,6 +941,7 @@ export class SynomemStorage implements SynomemRepository {
877
941
  kind: 'memo',
878
942
  title: event.subject,
879
943
  tags: event.tags,
944
+ topicIds: event.topicIds,
880
945
  visibility: event.visibility,
881
946
  status: 'unread',
882
947
  recipientAgentId: event.recipientAgentId,
@@ -889,6 +954,7 @@ export class SynomemStorage implements SynomemRepository {
889
954
  kind: 'note',
890
955
  title: event.title,
891
956
  tags: event.tags,
957
+ topicIds: event.topicIds,
892
958
  visibility: event.visibility,
893
959
  status: 'active',
894
960
  ownerAgentId: event.ownerAgentId,
@@ -905,18 +971,20 @@ export class SynomemStorage implements SynomemRepository {
905
971
  kind: 'post',
906
972
  title: event.title,
907
973
  tags: event.tags,
974
+ topicIds: event.topicIds,
908
975
  visibility: 'workspace',
909
976
  status: 'active',
910
977
  });
911
978
  } else if (event.type === 'post.edited') {
912
979
  this.db()
913
980
  .prepare(
914
- `UPDATE items_current SET title = ?, tags_json = ?,
981
+ `UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?,
915
982
  updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
916
983
  )
917
984
  .run(
918
985
  event.title,
919
986
  JSON.stringify(event.tags ?? []),
987
+ JSON.stringify(event.topicIds ?? []),
920
988
  sequence,
921
989
  event.createdAt,
922
990
  event.postId,
@@ -952,12 +1020,13 @@ export class SynomemStorage implements SynomemRepository {
952
1020
  } else if (event.type === 'note.revised') {
953
1021
  this.db()
954
1022
  .prepare(
955
- `UPDATE items_current SET title = ?, tags_json = ?, visibility = ?,
1023
+ `UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, visibility = ?,
956
1024
  updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
957
1025
  )
958
1026
  .run(
959
1027
  event.title,
960
1028
  JSON.stringify(event.tags ?? []),
1029
+ JSON.stringify(event.topicIds ?? []),
961
1030
  event.visibility,
962
1031
  sequence,
963
1032
  event.createdAt,
@@ -969,6 +1038,7 @@ export class SynomemStorage implements SynomemRepository {
969
1038
  kind: 'task',
970
1039
  title: event.title,
971
1040
  tags: event.tags,
1041
+ topicIds: event.topicIds,
972
1042
  visibility: event.visibility,
973
1043
  status: event.requiresAcceptance ? 'assigned' : 'open',
974
1044
  assigneeAgentId: event.assigneeAgentId,
@@ -978,12 +1048,13 @@ export class SynomemStorage implements SynomemRepository {
978
1048
  } else if (event.type === 'task.updated') {
979
1049
  this.db()
980
1050
  .prepare(
981
- `UPDATE items_current SET title = ?, tags_json = ?, visibility = ?, due_at = ?,
1051
+ `UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, visibility = ?, due_at = ?,
982
1052
  updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
983
1053
  )
984
1054
  .run(
985
1055
  event.title,
986
1056
  JSON.stringify(event.tags ?? []),
1057
+ JSON.stringify(event.topicIds ?? []),
987
1058
  event.visibility,
988
1059
  dueInstant(event.due) ?? null,
989
1060
  sequence,
@@ -1003,6 +1074,7 @@ export class SynomemStorage implements SynomemRepository {
1003
1074
  kind: 'todo',
1004
1075
  title: event.title,
1005
1076
  tags: event.tags,
1077
+ topicIds: event.topicIds,
1006
1078
  visibility: 'private',
1007
1079
  status: 'open',
1008
1080
  ownerAgentId: event.actor.id,
@@ -1012,12 +1084,13 @@ export class SynomemStorage implements SynomemRepository {
1012
1084
  } else if (event.type === 'todo.updated') {
1013
1085
  this.db()
1014
1086
  .prepare(
1015
- `UPDATE items_current SET title = ?, tags_json = ?, due_at = ?,
1087
+ `UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, due_at = ?,
1016
1088
  updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
1017
1089
  )
1018
1090
  .run(
1019
1091
  event.title,
1020
1092
  JSON.stringify(event.tags ?? []),
1093
+ JSON.stringify(event.topicIds ?? []),
1021
1094
  dueInstant(event.due) ?? null,
1022
1095
  sequence,
1023
1096
  event.createdAt,
@@ -1084,6 +1157,8 @@ export class SynomemStorage implements SynomemRepository {
1084
1157
  if (input.actorId) add('actor_id = ?', input.actorId);
1085
1158
  if (input.actorKind) add('actor_kind = ?', input.actorKind);
1086
1159
  if (input.tag) add('EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)', input.tag);
1160
+ if (input.topicId)
1161
+ add('EXISTS (SELECT 1 FROM json_each(topic_ids_json) WHERE value = ?)', input.topicId);
1087
1162
  if (input.status) add('status = ?', input.status);
1088
1163
  if (input.visibility) add('visibility = ?', input.visibility);
1089
1164
  if (input.revoked !== undefined) {
@@ -1269,6 +1344,8 @@ export class SynomemStorage implements SynomemRepository {
1269
1344
  if (input.actorId) add('actor_id = ?', input.actorId);
1270
1345
  if (input.actorKind) add('actor_kind = ?', input.actorKind);
1271
1346
  if (input.tag) add('EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)', input.tag);
1347
+ if (input.topicId)
1348
+ add('EXISTS (SELECT 1 FROM json_each(topic_ids_json) WHERE value = ?)', input.topicId);
1272
1349
  if (input.status) add('status = ?', input.status);
1273
1350
  if (input.pending) {
1274
1351
  add(`((kind = 'kudos' AND status = 'unacknowledged') OR
@@ -1687,6 +1764,8 @@ export class SynomemStorage implements SynomemRepository {
1687
1764
  const supportedTypes = new Set([
1688
1765
  'agent.created',
1689
1766
  'agent.updated',
1767
+ 'topic.created',
1768
+ 'topic.updated',
1690
1769
  'kudos.given',
1691
1770
  'kudos.acknowledged',
1692
1771
  'kudos.revoked',
@@ -1830,6 +1909,81 @@ export class SynomemStorage implements SynomemRepository {
1830
1909
  return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
1831
1910
  }
1832
1911
 
1912
+ /* -------------------------------------------------------------------- topics */
1913
+
1914
+ insertTopic(topic: Topic): void {
1915
+ const parsed = topicProfileSchema.parse(topic);
1916
+ this.db()
1917
+ .prepare(
1918
+ `INSERT INTO topics(id, display_name, status, created_at, updated_at)
1919
+ VALUES (?, ?, ?, ?, ?)`,
1920
+ )
1921
+ .run(parsed.id, parsed.displayName, parsed.status, parsed.createdAt, parsed.createdAt);
1922
+ this.insertTopicAliases(parsed.id, parsed.aliases ?? []);
1923
+ }
1924
+
1925
+ updateTopic(topic: Topic, updatedAt: string): void {
1926
+ const parsed = topicProfileSchema.parse(topic);
1927
+ this.db()
1928
+ .prepare('UPDATE topics SET display_name = ?, status = ?, updated_at = ? WHERE id = ?')
1929
+ .run(parsed.displayName, parsed.status, updatedAt, parsed.id);
1930
+ this.db().prepare('DELETE FROM topic_aliases WHERE topic_id = ?').run(parsed.id);
1931
+ this.insertTopicAliases(parsed.id, parsed.aliases ?? []);
1932
+ }
1933
+
1934
+ getTopic(idOrAlias: string): Topic | undefined {
1935
+ return this.resolveTopic(idOrAlias).match;
1936
+ }
1937
+
1938
+ listTopics(status?: 'active' | 'archived'): Topic[] {
1939
+ const rows = (status
1940
+ ? this.db()
1941
+ .prepare('SELECT * FROM topics WHERE status = ? ORDER BY display_name ASC')
1942
+ .all(status)
1943
+ : this.db()
1944
+ .prepare('SELECT * FROM topics ORDER BY display_name ASC')
1945
+ .all()) as unknown as TopicRow[];
1946
+ return rows.map((row) => this.topicFromRow(row));
1947
+ }
1948
+
1949
+ /** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
1950
+ resolveTopic(query: string): { match?: Topic; candidates: Topic[] } {
1951
+ const normalized = query.trim().toLowerCase();
1952
+ const rows = this.db()
1953
+ .prepare(
1954
+ `SELECT DISTINCT t.*
1955
+ FROM topics t
1956
+ LEFT JOIN topic_aliases x ON x.topic_id = t.id
1957
+ WHERE lower(t.id) = ? OR lower(t.display_name) = ? OR x.alias = ?
1958
+ ORDER BY t.id ASC`,
1959
+ )
1960
+ .all(normalized, normalized, normalized) as unknown as TopicRow[];
1961
+ const candidates = rows.map((row) => this.topicFromRow(row));
1962
+ return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
1963
+ }
1964
+
1965
+ private topicFromRow(row: TopicRow): Topic {
1966
+ const aliasRows = this.db()
1967
+ .prepare('SELECT alias FROM topic_aliases WHERE topic_id = ? ORDER BY alias ASC')
1968
+ .all(row.id) as unknown as Array<{ alias: string }>;
1969
+ const aliases = aliasRows.map((aliasRow) => aliasRow.alias);
1970
+ return {
1971
+ id: row.id,
1972
+ displayName: row.display_name,
1973
+ ...(aliases.length ? { aliases } : {}),
1974
+ status: row.status,
1975
+ createdAt: row.created_at,
1976
+ };
1977
+ }
1978
+
1979
+ private insertTopicAliases(topicId: string, aliases: string[]): void {
1980
+ for (const alias of aliases) {
1981
+ this.db()
1982
+ .prepare('INSERT INTO topic_aliases(alias, topic_id) VALUES (?, ?)')
1983
+ .run(alias.trim().toLowerCase(), topicId);
1984
+ }
1985
+ }
1986
+
1833
1987
  /* -------------------------------------------------------- post acknowledgment */
1834
1988
 
1835
1989
  listPostAcknowledgments(postId: string): PostAcknowledgment[] {