@wirecat/cli-core 0.418.0 → 0.420.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 (101) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/domain/providers.d.ts +20 -2
  3. package/dist/domain/providers.js +23 -3
  4. package/dist/domain/tag-consolidation.d.ts +30 -1
  5. package/dist/entities/index.d.ts +4 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/meetings/calendar-text.d.ts +13 -0
  9. package/dist/meetings/calendar-text.js +57 -0
  10. package/dist/meetings/event-store.d.ts +202 -0
  11. package/dist/meetings/event-store.js +1 -0
  12. package/dist/meetings/events.js +5 -1
  13. package/dist/meetings/index.d.ts +3 -0
  14. package/dist/meetings/index.js +2 -0
  15. package/dist/meetings/join-key.d.ts +7 -0
  16. package/dist/meetings/join-key.js +38 -0
  17. package/dist/meetings/model.d.ts +8 -2
  18. package/dist/meetings/read-answers.d.ts +6 -2
  19. package/dist/meetings/read-answers.js +9 -2
  20. package/dist/meetings/store.d.ts +2 -0
  21. package/dist/meetings/testing/event-store-contract.d.ts +10 -0
  22. package/dist/meetings/testing/event-store-contract.js +161 -0
  23. package/dist/meetings/testing/index.d.ts +1 -0
  24. package/dist/meetings/testing/index.js +1 -0
  25. package/dist/parity/bin.js +3 -1
  26. package/dist/search/account-list.js +39 -7
  27. package/dist/services/agenda.js +5 -5
  28. package/dist/services/document-source.d.ts +7 -3
  29. package/dist/services/document-source.js +24 -12
  30. package/dist/services/documents/ingestion.d.ts +6 -1
  31. package/dist/services/documents/ingestion.js +4 -0
  32. package/dist/services/notes-search.js +3 -2
  33. package/dist/services/search-all.js +1 -0
  34. package/dist/services/task-tag-filters.d.ts +3 -0
  35. package/dist/services/task-tag-filters.js +8 -0
  36. package/dist/services/tasks.js +5 -9
  37. package/dist/store/document-transfer/index.js +12 -2
  38. package/dist/store/index.d.ts +2 -1
  39. package/dist/store/sqlite/accounts.js +1 -0
  40. package/dist/store/sqlite/calendar.d.ts +3 -0
  41. package/dist/store/sqlite/calendar.js +593 -0
  42. package/dist/store/sqlite/catalog.js +16 -5
  43. package/dist/store/sqlite/chat-rekey.js +1 -1
  44. package/dist/store/sqlite/chats.js +2 -2
  45. package/dist/store/sqlite/context-document-source.js +8 -4
  46. package/dist/store/sqlite/document-transfer/apply.js +24 -2
  47. package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
  48. package/dist/store/sqlite/document-transfer/collection.js +66 -0
  49. package/dist/store/sqlite/document-transfer/plan.js +15 -8
  50. package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
  51. package/dist/store/sqlite/entity-types.d.ts +5 -1
  52. package/dist/store/sqlite/entity-types.js +5 -1
  53. package/dist/store/sqlite/events.d.ts +8 -2
  54. package/dist/store/sqlite/events.js +36 -12
  55. package/dist/store/sqlite/files.d.ts +93 -0
  56. package/dist/store/sqlite/files.js +293 -0
  57. package/dist/store/sqlite/folded.d.ts +3 -0
  58. package/dist/store/sqlite/folded.js +1 -0
  59. package/dist/store/sqlite/imported-documents.js +1 -1
  60. package/dist/store/sqlite/join-keys.d.ts +8 -0
  61. package/dist/store/sqlite/join-keys.js +27 -0
  62. package/dist/store/sqlite/knowledge.js +6 -6
  63. package/dist/store/sqlite/link-reviews.d.ts +9 -0
  64. package/dist/store/sqlite/link-reviews.js +12 -0
  65. package/dist/store/sqlite/lucene-fields.js +8 -6
  66. package/dist/store/sqlite/manifest.js +11 -0
  67. package/dist/store/sqlite/meeting-selection.js +1 -1
  68. package/dist/store/sqlite/meetings.js +6 -1
  69. package/dist/store/sqlite/migrations.generated.js +143 -0
  70. package/dist/store/sqlite/note-index.d.ts +8 -1
  71. package/dist/store/sqlite/note-index.js +14 -0
  72. package/dist/store/sqlite/note-search.js +6 -5
  73. package/dist/store/sqlite/notes.d.ts +18 -1
  74. package/dist/store/sqlite/notes.js +237 -40
  75. package/dist/store/sqlite/open-store.js +4 -0
  76. package/dist/store/sqlite/person-links.js +7 -2
  77. package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
  78. package/dist/store/sqlite/schema/canonical-tags.js +8 -0
  79. package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
  80. package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
  81. package/dist/store/sqlite/schema.d.ts +3589 -1634
  82. package/dist/store/sqlite/schema.js +191 -13
  83. package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
  84. package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
  85. package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
  86. package/dist/store/sqlite/tag-consolidation-input.js +52 -0
  87. package/dist/store/sqlite/tag-consolidation.js +51 -66
  88. package/dist/store/sqlite/tag-creation.js +17 -8
  89. package/dist/store/sqlite/tag-resolution.d.ts +5 -0
  90. package/dist/store/sqlite/tag-resolution.js +33 -0
  91. package/dist/store/sqlite/tags.d.ts +3 -1
  92. package/dist/store/sqlite/tags.js +70 -50
  93. package/dist/store/sqlite/tasks.d.ts +2 -0
  94. package/dist/store/sqlite/tasks.js +6 -1
  95. package/dist/store/sqlite/things.js +2 -1
  96. package/dist/store/sqlite/vocabulary.d.ts +1 -0
  97. package/dist/store/sqlite/vocabulary.js +30 -8
  98. package/dist/store/store.d.ts +6 -0
  99. package/dist/store/store.js +10 -0
  100. package/dist/store/tags.js +2 -2
  101. package/package.json +2 -1
@@ -596,5 +596,148 @@ export const GENERATED = [
596
596
  "INSERT OR IGNORE INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version, built_at, analyzer)\n VALUES ('transcript_index', 0, 0, 0, 1, NULL, NULL);",
597
597
  "INSERT OR IGNORE INTO transcript_index_pending (indexable_type, id)\n SELECT 'attachment', id FROM attachments WHERE attachable_type = 'message' AND coalesce(text, '') <> ''\n AND (coalesce(extractor, '') LIKE 'speech:%' OR kind IN ('voice', 'video_note') OR coalesce(mime, '') = 'audio/ogg');"
598
598
  ]
599
+ },
600
+ {
601
+ "name": "20261025064253_version-30-reviewed-tag-consolidation",
602
+ "statements": [
603
+ "CREATE TABLE `tag_consolidations` (\n\t`id` integer PRIMARY KEY,\n\t`source_id` integer NOT NULL,\n\t`survivor_id` integer NOT NULL,\n\t`source_member_ids` text NOT NULL,\n\t`survivor_member_ids` text NOT NULL,\n\t`actor_type` text NOT NULL,\n\t`actor_id` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`idempotency_key` text NOT NULL UNIQUE,\n\t`vocabulary_fingerprint` text NOT NULL,\n\t`preview_fingerprint` text NOT NULL,\n\t`counts` text NOT NULL,\n\t`request_fingerprint` text NOT NULL,\n\tCONSTRAINT `fk_tag_consolidations_source_id_canonical_tags_id_fk` FOREIGN KEY (`source_id`) REFERENCES `canonical_tags`(`id`),\n\tCONSTRAINT `fk_tag_consolidations_survivor_id_canonical_tags_id_fk` FOREIGN KEY (`survivor_id`) REFERENCES `canonical_tags`(`id`)\n);",
604
+ "CREATE TABLE `tag_redirects` (\n\t`source_id` integer PRIMARY KEY,\n\t`survivor_id` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\tCONSTRAINT `fk_tag_redirects_source_id_canonical_tags_id_fk` FOREIGN KEY (`source_id`) REFERENCES `canonical_tags`(`id`),\n\tCONSTRAINT `fk_tag_redirects_survivor_id_canonical_tags_id_fk` FOREIGN KEY (`survivor_id`) REFERENCES `canonical_tags`(`id`)\n);",
605
+ "ALTER TABLE `auto_tag_claims` RENAME TO `canonical_auto_tag_claims`;",
606
+ "ALTER TABLE `tags` RENAME TO `canonical_tags`;",
607
+ "DROP INDEX IF EXISTS `auto_tag_claims_by_tag_id`;",
608
+ "CREATE INDEX `canonical_auto_tag_claims_by_tag_id` ON `canonical_auto_tag_claims` (`tag_id`);",
609
+ "CREATE INDEX `tag_consolidations_by_source_id` ON `tag_consolidations` (`source_id`);",
610
+ "CREATE INDEX `tag_consolidations_by_survivor_id` ON `tag_consolidations` (`survivor_id`);",
611
+ "CREATE INDEX `tag_consolidations_by_actor_type_actor_id` ON `tag_consolidations` (`actor_type`,`actor_id`);",
612
+ "CREATE INDEX `tag_redirects_by_survivor_id` ON `tag_redirects` (`survivor_id`);"
613
+ ]
614
+ },
615
+ {
616
+ "name": "20261025064302_version-30-resolved-tag-view",
617
+ "statements": [
618
+ "CREATE VIEW resolved_tags AS\nSELECT original.id,canonical.name,canonical.kind,canonical.id AS canonical_id,original.created_at,original.updated_at\nFROM canonical_tags original\nLEFT JOIN tag_redirects r ON r.source_id=original.id\nJOIN canonical_tags canonical ON canonical.id=coalesce(r.survivor_id,original.id);"
619
+ ]
620
+ },
621
+ {
622
+ "name": "20261025070402_version-31-drive-account",
623
+ "statements": [
624
+ "-- Drive documents leave their stand-in notes folder (`folder`, named `google-drive:<principal>`) for the\n-- principal's `google-drive` account, by UPDATE, so every `document:<id>` reference holds. One folder per\n-- principal moves, the oldest; a duplicate, or one whose files the account already holds, stays a folder.\nCREATE TEMP TABLE drive_moves (folder_id INTEGER PRIMARY KEY, principal TEXT NOT NULL, account_id INTEGER);",
625
+ "INSERT INTO temp.drive_moves (folder_id, principal)\nSELECT f.id, substr(f.name, 14) FROM accounts f\nWHERE f.provider = 'folder' AND substr(f.name, 1, 13) = 'google-drive:' AND length(f.name) > 13\n AND f.id = (SELECT min(o.id) FROM accounts o WHERE o.provider = 'folder' AND o.name = f.name)\n AND NOT EXISTS (\n SELECT 1 FROM documents held JOIN accounts g ON g.id = held.account_id\n WHERE g.provider = 'google-drive' AND g.external_id = substr(f.name, 14)\n AND held.external_id IN (SELECT external_id FROM documents WHERE account_id = f.id)\n );",
626
+ "INSERT OR IGNORE INTO accounts (provider, external_id, name, scope, organization_id, created_at, updated_at)\nSELECT 'google-drive', m.principal, 'Google Drive', f.scope, f.organization_id, f.created_at, f.updated_at\nFROM temp.drive_moves m JOIN accounts f ON f.id = m.folder_id;",
627
+ "UPDATE temp.drive_moves SET account_id = (\n SELECT id FROM accounts WHERE provider = 'google-drive' AND external_id = drive_moves.principal\n);",
628
+ "UPDATE accounts SET\n settings = (\n SELECT json_set(\n CASE WHEN json_valid(accounts.settings) AND json_type(accounts.settings) = 'object' THEN accounts.settings ELSE '{}' END,\n '$.format', coalesce(CASE WHEN json_valid(f.settings) THEN json_extract(f.settings, '$.format') END, 'markdown'),\n '$.movedFrom', json_object('id', f.id, 'external_id', f.external_id, 'name', f.name)\n )\n FROM temp.drive_moves m JOIN accounts f ON f.id = m.folder_id WHERE m.account_id = accounts.id\n ),\n scope = (SELECT f.scope FROM temp.drive_moves m JOIN accounts f ON f.id = m.folder_id WHERE m.account_id = accounts.id),\n organization_id = (\n SELECT f.organization_id FROM temp.drive_moves m JOIN accounts f ON f.id = m.folder_id WHERE m.account_id = accounts.id\n )\nWHERE id IN (SELECT account_id FROM temp.drive_moves);",
629
+ "UPDATE account_identities SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = account_identities.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
630
+ "UPDATE aliases SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = aliases.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
631
+ "UPDATE bot_updates SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = bot_updates.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
632
+ "UPDATE chats SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = chats.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
633
+ "UPDATE chunks SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = chunks.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
634
+ "UPDATE documents SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = documents.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
635
+ "UPDATE email_threads SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = email_threads.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
636
+ "UPDATE emails SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = emails.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
637
+ "UPDATE involvements SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = involvements.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
638
+ "UPDATE mailboxes SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = mailboxes.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
639
+ "UPDATE mail_rules SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = mail_rules.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
640
+ "UPDATE meeting_series SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = meeting_series.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
641
+ "UPDATE meetings SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = meetings.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
642
+ "UPDATE messages SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = messages.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
643
+ "UPDATE projects SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = projects.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
644
+ "UPDATE proposed_actions SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = proposed_actions.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
645
+ "UPDATE reminders SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = reminders.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
646
+ "UPDATE sync_cursors SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = sync_cursors.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
647
+ "UPDATE syncs SET account_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = syncs.account_id)\nWHERE account_id IN (SELECT folder_id FROM temp.drive_moves);",
648
+ "UPDATE agent_actions SET actor_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = agent_actions.actor_id)\nWHERE actor_type = 'account' AND actor_id IN (SELECT folder_id FROM temp.drive_moves);",
649
+ "UPDATE agent_actions SET target_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = agent_actions.target_id)\nWHERE target_type = 'account' AND target_id IN (SELECT folder_id FROM temp.drive_moves);",
650
+ "UPDATE aliases SET aliasable_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = aliases.aliasable_id)\nWHERE aliasable_type = 'account' AND aliasable_id IN (SELECT folder_id FROM temp.drive_moves);",
651
+ "UPDATE attachments SET attachable_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = attachments.attachable_id)\nWHERE attachable_type = 'account' AND attachable_id IN (SELECT folder_id FROM temp.drive_moves);",
652
+ "UPDATE chunks SET chunkable_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = chunks.chunkable_id)\nWHERE chunkable_type = 'account' AND chunkable_id IN (SELECT folder_id FROM temp.drive_moves);",
653
+ "UPDATE involvements SET subject_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = involvements.subject_id)\nWHERE subject_type = 'account' AND subject_id IN (SELECT folder_id FROM temp.drive_moves);",
654
+ "UPDATE links SET from_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = links.from_id)\nWHERE from_type = 'account' AND from_id IN (SELECT folder_id FROM temp.drive_moves);",
655
+ "UPDATE links SET to_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = links.to_id)\nWHERE to_type = 'account' AND to_id IN (SELECT folder_id FROM temp.drive_moves);",
656
+ "UPDATE memories SET subject_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = memories.subject_id)\nWHERE subject_type = 'account' AND subject_id IN (SELECT folder_id FROM temp.drive_moves);",
657
+ "UPDATE notes SET notable_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = notes.notable_id)\nWHERE notable_type = 'account' AND notable_id IN (SELECT folder_id FROM temp.drive_moves);",
658
+ "UPDATE proposed_actions SET target_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = proposed_actions.target_id)\nWHERE target_type = 'account' AND target_id IN (SELECT folder_id FROM temp.drive_moves);",
659
+ "UPDATE taggings SET taggable_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = taggings.taggable_id)\nWHERE taggable_type = 'account' AND taggable_id IN (SELECT folder_id FROM temp.drive_moves);",
660
+ "UPDATE facts SET subject_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = facts.subject_id)\nWHERE subject_type = 'account' AND subject_id IN (SELECT folder_id FROM temp.drive_moves);",
661
+ "UPDATE mention_pending SET source_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = mention_pending.source_id)\nWHERE source_type = 'account' AND source_id IN (SELECT folder_id FROM temp.drive_moves);",
662
+ "UPDATE mentions SET source_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = mentions.source_id)\nWHERE source_type = 'account' AND source_id IN (SELECT folder_id FROM temp.drive_moves);",
663
+ "UPDATE mentions SET target_id = (SELECT account_id FROM temp.drive_moves WHERE folder_id = mentions.target_id)\nWHERE target_type = 'account' AND target_id IN (SELECT folder_id FROM temp.drive_moves);",
664
+ "-- A profile that named the folder names the account: `{ provider: \"google-drive\", account: <principal> }`.\nUPDATE store_settings SET value = (\n SELECT json_group_object(app.key, CASE WHEN app.type = 'object' THEN (\n SELECT json_group_object(profile.key, CASE\n WHEN profile.type = 'object' AND json_extract(profile.value, '$.provider') = 'folder'\n AND json_extract(profile.value, '$.account') IN (SELECT 'google-drive:' || principal FROM temp.drive_moves)\n THEN json_set(profile.value, '$.provider', 'google-drive', '$.account', substr(json_extract(profile.value, '$.account'), 14))\n WHEN profile.type IN ('object', 'array') THEN json(profile.value)\n ELSE profile.value END)\n FROM json_each(app.value) profile\n ) WHEN app.type = 'array' THEN json(app.value) ELSE app.value END)\n FROM json_each(store_settings.value) app\n)\nWHERE key = 'profiles' AND json_valid(value) AND json_type(value) = 'object'\n AND EXISTS (SELECT 1 FROM temp.drive_moves);",
665
+ "-- A group member `notes:<folder id>` follows the documents to `notes:<account id>`.\nUPDATE store_settings SET value = (\n SELECT json_group_object(grp.key, CASE WHEN grp.type = 'array' THEN (\n SELECT json_group_array(coalesce(\n (SELECT 'notes:' || m.account_id FROM temp.drive_moves m WHERE 'notes:' || m.folder_id = member.value),\n member.value))\n FROM json_each(grp.value) member\n ) WHEN grp.type = 'object' THEN json(grp.value) ELSE grp.value END)\n FROM json_each(store_settings.value) grp\n)\nWHERE key = 'accountGroups' AND json_valid(value) AND json_type(value) = 'object'\n AND EXISTS (SELECT 1 FROM temp.drive_moves);",
666
+ "DELETE FROM accounts WHERE id IN (SELECT folder_id FROM temp.drive_moves);",
667
+ "DROP TABLE temp.drive_moves;"
668
+ ]
669
+ },
670
+ {
671
+ "name": "20261025071848_version-32-files",
672
+ "statements": [
673
+ "CREATE TABLE `entity_link_events` (\n\t`id` integer PRIMARY KEY,\n\t`linkable_type` text NOT NULL,\n\t`linkable_id` integer NOT NULL,\n\t`entity_type` text NOT NULL,\n\t`from_entity_id` integer,\n\t`to_entity_id` integer,\n\t`method` text NOT NULL,\n\t`confidence` real,\n\t`author` text NOT NULL,\n\t`created_at` integer NOT NULL\n);",
674
+ "CREATE TABLE `files` (\n\t`id` integer PRIMARY KEY,\n\t`account_id` integer NOT NULL,\n\t`external_id` text NOT NULL,\n\t`kind` text NOT NULL,\n\t`name` text NOT NULL,\n\t`path` text NOT NULL,\n\t`path_folded` text,\n\t`parent_id` integer,\n\t`document_id` integer,\n\t`mime` text,\n\t`size` integer,\n\t`content_hash` text,\n\t`url` text,\n\t`external_created_at` integer,\n\t`external_updated_at` integer,\n\t`metadata` text,\n\t`seen_at` integer,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_files_account_id_accounts_id_fk` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`),\n\tCONSTRAINT `fk_files_parent_id_files_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `files`(`id`),\n\tCONSTRAINT `fk_files_document_id_documents_id_fk` FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`),\n\tCONSTRAINT `files_account_id_external_id_unique` UNIQUE(`account_id`,`external_id`)\n);",
675
+ "CREATE TABLE `link_reviews` (\n\t`id` integer PRIMARY KEY,\n\t`linkable_type` text NOT NULL,\n\t`linkable_id` integer NOT NULL,\n\t`entity_type` text NOT NULL,\n\t`candidates` text NOT NULL,\n\t`reason` text NOT NULL,\n\t`status` text NOT NULL,\n\t`chosen_id` integer,\n\t`decided_by` text,\n\t`decided_at` integer,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\tCONSTRAINT `link_reviews_linkable_type_linkable_id_entity_type_unique` UNIQUE(`linkable_type`,`linkable_id`,`entity_type`)\n);",
676
+ "ALTER TABLE `documents` ADD `pinned_file_id` integer;",
677
+ "ALTER TABLE `identities` ADD `email` text;",
678
+ "CREATE INDEX `entity_link_events_by_linkable_type_linkable_id` ON `entity_link_events` (`linkable_type`,`linkable_id`);",
679
+ "CREATE INDEX `entity_link_events_by_entity_type_to_entity_id` ON `entity_link_events` (`entity_type`,`to_entity_id`);",
680
+ "CREATE INDEX `files_by_account_id_path_folded` ON `files` (`account_id`,`path_folded`);",
681
+ "CREATE INDEX `files_path_to_fold` ON `files` (`id`) WHERE path_folded IS NULL;",
682
+ "CREATE INDEX `files_by_parent_id` ON `files` (`parent_id`);",
683
+ "CREATE INDEX `files_by_document_id` ON `files` (`document_id`);",
684
+ "CREATE INDEX `identities_by_email` ON `identities` (`email`) WHERE email IS NOT NULL;",
685
+ "CREATE INDEX `link_reviews_by_status_created_at` ON `link_reviews` (`status`,`created_at`);"
686
+ ]
687
+ },
688
+ {
689
+ "name": "20261025071850_version-32-files-data",
690
+ "statements": [
691
+ "-- A folder account's provider says what the folder is. Drive's stand-in folders (`google-drive:<principal>`) keep\n-- `folder`: version 31 moves their documents to the `google-drive` account, and they are no local folder either way.\nUPDATE accounts SET provider = CASE\n WHEN json_valid(settings) AND json_extract(settings, '$.format') = 'markdown' THEN 'local' ELSE 'obsidian' END\n WHERE provider = 'folder' AND substr(coalesce(name, ''), 1, 13) <> 'google-drive:';",
692
+ "UPDATE identities SET email = lower(trim(external_id)) WHERE provider = 'email' AND instr(external_id, '@') > 1;",
693
+ "-- A document hard-deleted (a rename onto a gone note's path) takes its file row along; the rename writes its own.\nCREATE TRIGGER files_document_bd BEFORE DELETE ON documents BEGIN\n DELETE FROM files WHERE document_id = old.id;\nEND;",
694
+ "-- One `files` row per document is written in JS on open (`fillFiles`), from the document after this id.\nINSERT OR REPLACE INTO store_settings (key, value, at) VALUES ('files_backfill_after', '0', unixepoch() * 1000);",
695
+ "-- A mail contact's address is its external id; every writer of one gets the column filled the same way.\nCREATE TRIGGER identities_email_ai AFTER INSERT ON identities\n WHEN new.provider = 'email' AND new.email IS NULL AND instr(new.external_id, '@') > 1 BEGIN\n UPDATE identities SET email = lower(trim(new.external_id)) WHERE id = new.id;\nEND;",
696
+ "-- A deleted document keeps its tags: a file moved between folders is revived with them (owner's choice). Its tags go\n-- when the row itself does (`document_bd`), and readers leave out a deleted document's taggings.\nDROP TRIGGER document_tombstone;"
697
+ ]
698
+ },
699
+ {
700
+ "name": "20261025072643_version-33-calendars",
701
+ "statements": [
702
+ "CREATE TABLE `calendar_entries` (\n\t`id` integer PRIMARY KEY,\n\t`calendar_id` integer NOT NULL,\n\t`kind` text NOT NULL,\n\t`series_entry_id` integer,\n\t`event_id` integer,\n\t`event_series_id` integer,\n\t`external_id` text NOT NULL,\n\t`ical_uid` text,\n\t`original_starts_at` integer,\n\t`exception` integer NOT NULL,\n\t`status` text NOT NULL,\n\t`title` text,\n\t`description` text,\n\t`location` text,\n\t`starts_at` integer,\n\t`ends_at` integer,\n\t`all_day` integer NOT NULL,\n\t`start_date` text,\n\t`end_date` text,\n\t`timezone` text,\n\t`recurrence` text,\n\t`recurrence_ends_at` integer,\n\t`event_type` text,\n\t`transparency` text,\n\t`visibility` text,\n\t`own_response` text,\n\t`organizer_identity_id` integer,\n\t`join_url` text,\n\t`join_key` text,\n\t`conference` text,\n\t`url` text,\n\t`sequence` integer,\n\t`etag` text,\n\t`external_updated_at` integer,\n\t`metadata` text,\n\t`seen_at` integer,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_calendar_entries_calendar_id_calendars_id_fk` FOREIGN KEY (`calendar_id`) REFERENCES `calendars`(`id`),\n\tCONSTRAINT `fk_calendar_entries_series_entry_id_calendar_entries_id_fk` FOREIGN KEY (`series_entry_id`) REFERENCES `calendar_entries`(`id`),\n\tCONSTRAINT `fk_calendar_entries_event_id_events_id_fk` FOREIGN KEY (`event_id`) REFERENCES `events`(`id`),\n\tCONSTRAINT `fk_calendar_entries_event_series_id_event_series_id_fk` FOREIGN KEY (`event_series_id`) REFERENCES `event_series`(`id`),\n\tCONSTRAINT `fk_calendar_entries_organizer_identity_id_identities_id_fk` FOREIGN KEY (`organizer_identity_id`) REFERENCES `identities`(`id`),\n\tCONSTRAINT `calendar_entries_calendar_id_external_id_unique` UNIQUE(`calendar_id`,`external_id`),\n\tCONSTRAINT `calendar_entries_series_entry_id_original_starts_at_unique` UNIQUE(`series_entry_id`,`original_starts_at`)\n);",
703
+ "CREATE TABLE `calendar_entry_participants` (\n\t`id` integer PRIMARY KEY,\n\t`entry_id` integer NOT NULL,\n\t`identity_id` integer,\n\t`email` text NOT NULL,\n\t`name` text,\n\t`response` text,\n\t`optional` integer NOT NULL,\n\t`organizer` integer NOT NULL,\n\t`self` integer NOT NULL,\n\t`resource` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\tCONSTRAINT `fk_calendar_entry_participants_entry_id_calendar_entries_id_fk` FOREIGN KEY (`entry_id`) REFERENCES `calendar_entries`(`id`),\n\tCONSTRAINT `fk_calendar_entry_participants_identity_id_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities`(`id`),\n\tCONSTRAINT `calendar_entry_participants_entry_id_email_unique` UNIQUE(`entry_id`,`email`)\n);",
704
+ "CREATE TABLE `calendar_entry_revisions` (\n\t`id` integer PRIMARY KEY,\n\t`entry_id` integer NOT NULL,\n\t`title` text,\n\t`starts_at` integer,\n\t`ends_at` integer,\n\t`location` text,\n\t`status` text,\n\t`recurrence` text,\n\t`recurrence_ends_at` integer,\n\t`external_updated_at` integer,\n\t`created_at` integer NOT NULL,\n\tCONSTRAINT `fk_calendar_entry_revisions_entry_id_calendar_entries_id_fk` FOREIGN KEY (`entry_id`) REFERENCES `calendar_entries`(`id`)\n);",
705
+ "CREATE TABLE `calendars` (\n\t`id` integer PRIMARY KEY,\n\t`account_id` integer NOT NULL,\n\t`external_id` text NOT NULL,\n\t`title` text,\n\t`timezone` text,\n\t`access_role` text,\n\t`primary_calendar` integer NOT NULL,\n\t`synced` integer NOT NULL,\n\t`seen_at` integer,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_calendars_account_id_accounts_id_fk` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`),\n\tCONSTRAINT `calendars_account_id_external_id_unique` UNIQUE(`account_id`,`external_id`)\n);",
706
+ "CREATE TABLE `event_index_pending` (\n\t`id` integer NOT NULL,\n\t`indexable_type` text NOT NULL,\n\tCONSTRAINT `event_index_pending_pk` PRIMARY KEY(`indexable_type`, `id`)\n);",
707
+ "ALTER TABLE `events` ADD `status` text;",
708
+ "ALTER TABLE `events` ADD `all_day` integer;",
709
+ "ALTER TABLE `events` ADD `source_deleted_at` integer;",
710
+ "ALTER TABLE `meetings` ADD `join_key` text;",
711
+ "CREATE INDEX `calendar_entries_by_calendar_id_starts_at` ON `calendar_entries` (`calendar_id`,`starts_at`);",
712
+ "CREATE INDEX `calendar_entries_by_ical_uid` ON `calendar_entries` (`ical_uid`) WHERE ical_uid IS NOT NULL;",
713
+ "CREATE INDEX `calendar_entries_by_join_key` ON `calendar_entries` (`join_key`) WHERE join_key IS NOT NULL;",
714
+ "CREATE INDEX `calendar_entries_by_event_id` ON `calendar_entries` (`event_id`);",
715
+ "CREATE INDEX `calendar_entries_by_event_series_id` ON `calendar_entries` (`event_series_id`);",
716
+ "CREATE INDEX `calendar_entries_by_event_type_event_id` ON `calendar_entries` (`event_type`,`event_id`);",
717
+ "CREATE INDEX `calendar_entries_by_organizer_identity_id` ON `calendar_entries` (`organizer_identity_id`);",
718
+ "CREATE INDEX `calendar_entry_participants_by_identity_id` ON `calendar_entry_participants` (`identity_id`);",
719
+ "CREATE INDEX `calendar_entry_revisions_by_entry_id_created_at` ON `calendar_entry_revisions` (`entry_id`,`created_at`);",
720
+ "CREATE INDEX `meetings_by_join_key` ON `meetings` (`join_key`) WHERE join_key IS NOT NULL;",
721
+ "CREATE INDEX `meetings_join_key_to_fill` ON `meetings` (`id`) WHERE join_url IS NOT NULL AND join_key IS NULL;"
722
+ ]
723
+ },
724
+ {
725
+ "name": "20261025072644_version-33-calendars-triggers",
726
+ "statements": [
727
+ "-- The event is indexed, not each entry: its title, description and location, and the names its entries list.\n-- Stemming needs JS, so the triggers only queue the event and the next search indexes it, as tasks do.\nCREATE VIRTUAL TABLE event_words USING fts5(\n normalized_text, scope,\n content = '', contentless_delete = 1,\n tokenize = 'unicode61 remove_diacritics 2', prefix = '3');",
728
+ "CREATE VIRTUAL TABLE event_words_vocab USING fts5vocab(event_words, 'col');",
729
+ "CREATE VIRTUAL TABLE event_stems USING fts5(\n stems, scope,\n content = '', contentless_delete = 1,\n tokenize = 'unicode61 remove_diacritics 2');",
730
+ "CREATE TRIGGER event_index_ai AFTER INSERT ON events BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id) VALUES ('event', new.id);\nEND;",
731
+ "CREATE TRIGGER event_index_au AFTER UPDATE OF title, description, location, deleted_at ON events\n WHEN old.title IS NOT new.title OR old.description IS NOT new.description OR old.location IS NOT new.location\n OR old.deleted_at IS NOT new.deleted_at BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id) VALUES ('event', new.id);\nEND;",
732
+ "CREATE TRIGGER event_index_ad AFTER DELETE ON events BEGIN\n DELETE FROM event_words WHERE rowid = old.id;\n DELETE FROM event_stems WHERE rowid = old.id;\n DELETE FROM event_index_pending WHERE indexable_type = 'event' AND id = old.id;\nEND;",
733
+ "-- An entry that joins or leaves an event, or changes kind, changes what the event is found by.\nCREATE TRIGGER event_index_entry_au AFTER UPDATE OF event_id, event_type, deleted_at ON calendar_entries\n WHEN old.event_id IS NOT new.event_id OR old.event_type IS NOT new.event_type OR old.deleted_at IS NOT new.deleted_at BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id)\n SELECT 'event', value FROM (SELECT old.event_id AS value UNION SELECT new.event_id) WHERE value IS NOT NULL;\nEND;",
734
+ "CREATE TRIGGER event_index_participant_ai AFTER INSERT ON calendar_entry_participants BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id)\n SELECT 'event', event_id FROM calendar_entries WHERE id = new.entry_id AND event_id IS NOT NULL;\nEND;",
735
+ "CREATE TRIGGER event_index_participant_au AFTER UPDATE OF name ON calendar_entry_participants\n WHEN old.name IS NOT new.name BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id)\n SELECT 'event', event_id FROM calendar_entries WHERE id = new.entry_id AND event_id IS NOT NULL;\nEND;",
736
+ "CREATE TRIGGER event_index_participant_ad AFTER DELETE ON calendar_entry_participants BEGIN\n INSERT OR IGNORE INTO event_index_pending (indexable_type, id)\n SELECT 'event', event_id FROM calendar_entries WHERE id = old.entry_id AND event_id IS NOT NULL;\nEND;",
737
+ "CREATE TRIGGER calendar_entries_time_bi BEFORE INSERT ON calendar_entries\n WHEN new.ends_at < new.starts_at BEGIN\n SELECT RAISE(ABORT, 'a calendar entry cannot end before it starts');\nEND;",
738
+ "CREATE TRIGGER calendar_entries_time_bu BEFORE UPDATE OF starts_at, ends_at ON calendar_entries\n WHEN new.ends_at < new.starts_at BEGIN\n SELECT RAISE(ABORT, 'a calendar entry cannot end before it starts');\nEND;",
739
+ "INSERT OR IGNORE INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version, built_at, analyzer)\n VALUES ('event_index', 0, 0, 0, 1, NULL, NULL);",
740
+ "INSERT OR IGNORE INTO event_index_pending (indexable_type, id) SELECT 'event', id FROM events;"
741
+ ]
599
742
  }
600
743
  ];
@@ -15,7 +15,7 @@ export interface NoteIndexState {
15
15
  */
16
16
  export interface Corpus {
17
17
  /** The singular table name: `indexable_type` in the queue, `chunkable_type` in chunks. */
18
- type: "document" | "note" | "memory" | "email" | "task" | "transcript";
18
+ type: "document" | "note" | "memory" | "email" | "task" | "transcript" | "event";
19
19
  /** Its row in `search_index_state`. */
20
20
  index: string;
21
21
  table: string;
@@ -76,6 +76,13 @@ export declare const CORPORA: {
76
76
  readonly read: string;
77
77
  readonly pieces: false;
78
78
  };
79
+ readonly event: {
80
+ readonly type: "event";
81
+ readonly index: "event_index";
82
+ readonly table: "events";
83
+ readonly read: string;
84
+ readonly pieces: false;
85
+ };
79
86
  };
80
87
  /** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
81
88
  export declare const noteIndexText: (title: string | null, text: string) => string;
@@ -55,6 +55,20 @@ export const CORPORA = {
55
55
  "'personal' AS scope FROM attachments a WHERE a.id = ?",
56
56
  pieces: false,
57
57
  },
58
+ event: {
59
+ type: "event",
60
+ index: "event_index",
61
+ table: "events",
62
+ // Working location, focus time, out of office and birthdays are listed, never searched (requirements 3.4).
63
+ read: "SELECT e.title, trim(coalesce(e.description, '') || ' ' || coalesce(e.location, '') || ' ' || coalesce((" +
64
+ "SELECT group_concat(p.name, ' ') FROM calendar_entries ce JOIN calendar_entry_participants p ON p.entry_id = ce.id " +
65
+ "WHERE ce.event_id = e.id AND ce.deleted_at IS NULL AND p.resource = 0 AND p.name IS NOT NULL), '')) AS body, " +
66
+ "e.deleted_at IS NULL AND NOT EXISTS (SELECT 1 FROM calendar_entries ce WHERE ce.event_id = e.id " +
67
+ "AND ce.deleted_at IS NULL AND ce.event_type IN ('workingLocation', 'focusTime', 'outOfOffice', 'birthday')) AS live, " +
68
+ "coalesce((SELECT a.scope FROM calendar_entries ce JOIN calendars c ON c.id = ce.calendar_id " +
69
+ "JOIN accounts a ON a.id = c.account_id WHERE ce.event_id = e.id LIMIT 1), 'personal') AS scope FROM events e WHERE e.id = ?",
70
+ pieces: false,
71
+ },
58
72
  };
59
73
  /** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
60
74
  export const noteIndexText = (title, text) => (title ? `${title}\n\n${text}` : text);
@@ -10,6 +10,7 @@ import { prefixOf } from "./lucene.js";
10
10
  import { drainNoteIndex, noteIndexState, noteIndexText } from "./note-index.js";
11
11
  import { documentOf, noteOf } from "./notes.js";
12
12
  import { stemmerCache } from "./stems.js";
13
+ import { canonicalTagNamed } from "./tags.js";
13
14
  import { read } from "./transaction.js";
14
15
  import { dot } from "./vectors.js";
15
16
  /** The fields a note has. A message-only field (`from:`, `chat:`, …) is refused rather than matching nothing. */
@@ -41,9 +42,9 @@ const CORPORA = [
41
42
  view: "(SELECT id, account_id AS folder_id, external_id AS path, title, coalesce(body, '') AS text, metadata, updated_at, deleted_at FROM documents)",
42
43
  words: "document_words",
43
44
  stems: "document_stems",
44
- tagged: (tag) => bound("(n.id IN (SELECT g.taggable_id FROM taggings g JOIN tags t ON t.id = g.tag_id WHERE t.name = ? AND g.taggable_type = 'document') " +
45
- "OR n.folder_id IN (SELECT g.taggable_id FROM taggings g JOIN tags t ON t.id = g.tag_id WHERE t.name = ? AND g.taggable_type = 'account') " +
46
- "OR EXISTS (SELECT 1 FROM links l JOIN tags t ON t.id = l.to_id WHERE l.from_type = 'account' AND l.from_id = n.folder_id " +
45
+ tagged: (tag) => bound("(n.id IN (SELECT g.taggable_id FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE t.name = ? AND g.taggable_type = 'document') " +
46
+ "OR n.folder_id IN (SELECT g.taggable_id FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE t.name = ? AND g.taggable_type = 'account') " +
47
+ "OR EXISTS (SELECT 1 FROM links l JOIN resolved_tags t ON t.id=l.to_id WHERE l.from_type = 'account' AND l.from_id = n.folder_id " +
47
48
  `AND l.kind = ${linkKind("labelled")} AND l.to_type = 'tag' AND t.name = ? AND substr(n.path, 1, length(l.anchor) + 1) = l.anchor || '/'))`, tag, tag, tag),
48
49
  },
49
50
  {
@@ -51,7 +52,7 @@ const CORPORA = [
51
52
  view: "(SELECT id, NULL AS folder_id, NULL AS path, title, body AS text, NULL AS metadata, updated_at, deleted_at FROM notes)",
52
53
  words: "note_words",
53
54
  stems: "note_stems",
54
- tagged: (tag) => bound("n.id IN (SELECT g.taggable_id FROM taggings g JOIN tags t ON t.id = g.tag_id WHERE t.name = ? AND g.taggable_type = 'note')", tag),
55
+ tagged: (tag) => bound("n.id IN (SELECT g.taggable_id FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE t.name = ? AND g.taggable_type = 'note')", tag),
55
56
  },
56
57
  ];
57
58
  /** Notes matching a parsed query, by the same language as messages: files and written notes, merged. */
@@ -166,7 +167,7 @@ const searchCorpus = (context, query, corpus) => {
166
167
  if (tag === undefined)
167
168
  searchError("query.invalid-tag", node.span);
168
169
  // A label on a folder or subfolder labels every note under it, at any depth.
169
- return corpus.tagged(tag);
170
+ return corpus.tagged(canonicalTagNamed(database, tag)?.name ?? tag);
170
171
  }
171
172
  if (field === "source")
172
173
  return bound(value.toLowerCase() === (corpus.type === "note" ? "internal" : "file") ? "1" : "0");
@@ -9,12 +9,21 @@ import { type Label } from "./tags.js";
9
9
  import { type Thing } from "./things.js";
10
10
  export declare const NOTE_FORMATS: readonly ["obsidian", "markdown"];
11
11
  export declare const LINK_ORIGINS: readonly ["file", "owner", "suggested", "message"];
12
- /** A notes folder: an `accounts` row of provider `folder`. Where it is on disk is this computer's config. */
12
+ /**
13
+ * A notes folder: an `accounts` row of provider `local` or `obsidian`. Where it is on disk is this computer's config. A
14
+ * documents account (Drive's, one per Google account) is read and written the same way and names its `account`.
15
+ */
13
16
  export interface NoteFolder {
14
17
  id: string;
15
18
  name: string;
19
+ /** `obsidian` for a vault, `local` for a plain folder; `folder` for Drive's stand-in folders from before. */
20
+ provider: string;
16
21
  format: (typeof NOTE_FORMATS)[number];
17
22
  createdAt: string;
23
+ account?: {
24
+ provider: string;
25
+ account: string;
26
+ };
18
27
  }
19
28
  /**
20
29
  * A file in a notes folder (a `documents` row, `source` file) or a note the owner wrote (a `notes` row,
@@ -97,7 +106,15 @@ export interface NotesStore extends NoteSearch, ImportedDocuments {
97
106
  name: string;
98
107
  format?: NoteFolder["format"];
99
108
  }): Promise<NoteFolder>;
109
+ /** The notes folders only; a documents account is not one (`documentsAccounts`). */
100
110
  folders(): Promise<NoteFolder[]>;
111
+ /** One documents account by its key (`{ provider: "google-drive", account: <principal> }`), created when asked. */
112
+ documentsAccount(key: {
113
+ provider: string;
114
+ account: string;
115
+ }, create?: boolean): Promise<NoteFolder | undefined>;
116
+ /** Every documents account the store holds, by provider then account. */
117
+ documentsAccounts(): Promise<NoteFolder[]>;
101
118
  saveFileNote(input: FileNoteInput): Promise<{
102
119
  note: Note;
103
120
  changed: boolean;