@3sln/trove 0.0.2

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 (162) hide show
  1. package/README.md +1227 -0
  2. package/package.json +75 -0
  3. package/packages/core/src/collections/index.js +249 -0
  4. package/packages/core/src/errors.js +186 -0
  5. package/packages/core/src/identity/discovery.js +210 -0
  6. package/packages/core/src/identity/index.js +188 -0
  7. package/packages/core/src/identity/jwt.js +199 -0
  8. package/packages/core/src/index.js +104 -0
  9. package/packages/core/src/indexers/contribution.js +115 -0
  10. package/packages/core/src/indexers/registry.js +162 -0
  11. package/packages/core/src/indexing.js +340 -0
  12. package/packages/core/src/issues.js +150 -0
  13. package/packages/core/src/kv.js +0 -0
  14. package/packages/core/src/links.js +141 -0
  15. package/packages/core/src/metadata/cursor.js +73 -0
  16. package/packages/core/src/metadata/interface.js +244 -0
  17. package/packages/core/src/metadata/memory.js +270 -0
  18. package/packages/core/src/metadata/sqlite.js +412 -0
  19. package/packages/core/src/notifications/index.js +139 -0
  20. package/packages/core/src/notifications/webpush.js +217 -0
  21. package/packages/core/src/plugins/contributions.js +177 -0
  22. package/packages/core/src/plugins/identity.js +98 -0
  23. package/packages/core/src/plugins/index.js +225 -0
  24. package/packages/core/src/plugins/indexers.js +142 -0
  25. package/packages/core/src/plugins/installStore.js +134 -0
  26. package/packages/core/src/plugins/package.js +102 -0
  27. package/packages/core/src/plugins/packageStore.js +61 -0
  28. package/packages/core/src/plugins/runtime.js +101 -0
  29. package/packages/core/src/plugins/sql.js +52 -0
  30. package/packages/core/src/retry.js +74 -0
  31. package/packages/core/src/scan.js +302 -0
  32. package/packages/core/src/search/embeddings.js +128 -0
  33. package/packages/core/src/search/index.js +200 -0
  34. package/packages/core/src/search/keywordStore.js +107 -0
  35. package/packages/core/src/search/sqliteStores.js +455 -0
  36. package/packages/core/src/search/tagMatch.js +59 -0
  37. package/packages/core/src/search/transformer.js +195 -0
  38. package/packages/core/src/search/vectorStore.js +274 -0
  39. package/packages/core/src/search/vectorize.js +249 -0
  40. package/packages/core/src/sidecar/document.js +213 -0
  41. package/packages/core/src/sidecar/index.js +174 -0
  42. package/packages/core/src/sidecar/manager.js +239 -0
  43. package/packages/core/src/sidecar/store.js +46 -0
  44. package/packages/core/src/signedUrls.js +170 -0
  45. package/packages/core/src/sqlite-d1.js +162 -0
  46. package/packages/core/src/sqlite-driver.js +42 -0
  47. package/packages/core/src/sqlite.js +162 -0
  48. package/packages/core/src/storage/filesystem.js +283 -0
  49. package/packages/core/src/storage/interface.js +222 -0
  50. package/packages/core/src/storage/memory.js +113 -0
  51. package/packages/core/src/storage/prefixed.js +75 -0
  52. package/packages/core/src/storage/s3.js +316 -0
  53. package/packages/core/src/storage/s3sigv4.js +185 -0
  54. package/packages/core/src/tasks.js +228 -0
  55. package/packages/core/src/uploads.js +386 -0
  56. package/packages/core/src/util.js +125 -0
  57. package/packages/core/src/vfs.js +666 -0
  58. package/packages/plugin-sdk/src/browser.js +316 -0
  59. package/packages/plugin-sdk/src/index.js +32 -0
  60. package/packages/plugin-sdk/src/protocol.js +59 -0
  61. package/packages/plugin-sdk/src/rpc.js +95 -0
  62. package/packages/server/src/adapters/bun.js +78 -0
  63. package/packages/server/src/adapters/node.js +115 -0
  64. package/packages/server/src/adapters/staticAssets.js +123 -0
  65. package/packages/server/src/adapters/webDist.js +70 -0
  66. package/packages/server/src/adapters/worker-tasks.js +206 -0
  67. package/packages/server/src/adapters/worker.js +159 -0
  68. package/packages/server/src/cachePolicy.js +34 -0
  69. package/packages/server/src/engine/README.md +88 -0
  70. package/packages/server/src/engine/actions/scanCollection.js +114 -0
  71. package/packages/server/src/engine/index.js +95 -0
  72. package/packages/server/src/engine/lazy.js +25 -0
  73. package/packages/server/src/engine/providers/access.js +363 -0
  74. package/packages/server/src/engine/providers/core.js +405 -0
  75. package/packages/server/src/engine/providers/scan.js +67 -0
  76. package/packages/server/src/index.js +698 -0
  77. package/packages/server/src/manifest.js +98 -0
  78. package/packages/server/src/mcp/auth.js +40 -0
  79. package/packages/server/src/mcp/index.js +213 -0
  80. package/packages/server/src/mcp/protocol.js +181 -0
  81. package/packages/server/src/mcp/tools.js +351 -0
  82. package/packages/server/src/router.js +229 -0
  83. package/packages/server/src/routes.js +1066 -0
  84. package/packages/server/src/scope.js +43 -0
  85. package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
  86. package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
  87. package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
  88. package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
  89. package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
  90. package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
  91. package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
  92. package/packages/web/dist/icon.svg +11 -0
  93. package/packages/web/dist/index.html +16 -0
  94. package/packages/web/dist/sql-wasm.wasm +0 -0
  95. package/packages/web/dist/sw.js +186 -0
  96. package/packages/web/src/bl/actions.js +410 -0
  97. package/packages/web/src/bl/activity.js +306 -0
  98. package/packages/web/src/bl/commands.js +274 -0
  99. package/packages/web/src/bl/fileType.js +49 -0
  100. package/packages/web/src/bl/index.js +70 -0
  101. package/packages/web/src/bl/links.js +54 -0
  102. package/packages/web/src/bl/offline.js +268 -0
  103. package/packages/web/src/bl/openers.js +71 -0
  104. package/packages/web/src/bl/pluginInstall.js +59 -0
  105. package/packages/web/src/bl/services.js +143 -0
  106. package/packages/web/src/bl/social.js +234 -0
  107. package/packages/web/src/bl/tagQuery.js +44 -0
  108. package/packages/web/src/main.js +10 -0
  109. package/packages/web/src/platform/api.js +529 -0
  110. package/packages/web/src/platform/commands.js +89 -0
  111. package/packages/web/src/platform/context.js +77 -0
  112. package/packages/web/src/platform/contributions.js +156 -0
  113. package/packages/web/src/platform/index.js +150 -0
  114. package/packages/web/src/platform/keybindings.js +199 -0
  115. package/packages/web/src/platform/mediaUrls.js +137 -0
  116. package/packages/web/src/platform/navigation.js +131 -0
  117. package/packages/web/src/platform/notifications.js +50 -0
  118. package/packages/web/src/platform/overlay.js +81 -0
  119. package/packages/web/src/platform/pluginClientDb.js +132 -0
  120. package/packages/web/src/platform/pluginDock.js +141 -0
  121. package/packages/web/src/platform/pluginFrames.js +194 -0
  122. package/packages/web/src/platform/pluginHost.js +648 -0
  123. package/packages/web/src/platform/pluginMedia.js +62 -0
  124. package/packages/web/src/platform/pluginModules.js +90 -0
  125. package/packages/web/src/platform/pluginNet.js +71 -0
  126. package/packages/web/src/platform/pluginPackage.js +247 -0
  127. package/packages/web/src/platform/pluginRpc.js +377 -0
  128. package/packages/web/src/platform/pluginSigning.js +168 -0
  129. package/packages/web/src/platform/pluginStore.js +67 -0
  130. package/packages/web/src/platform/settings.js +101 -0
  131. package/packages/web/src/platform/spatialNav.js +286 -0
  132. package/packages/web/src/platform/viewport.js +123 -0
  133. package/packages/web/src/platform/voice.js +133 -0
  134. package/packages/web/src/platform/voiceSearch.js +155 -0
  135. package/packages/web/src/platform/whenclause.js +162 -0
  136. package/packages/web/src/platform/workbench.js +156 -0
  137. package/packages/web/src/runtime.js +73 -0
  138. package/packages/web/src/styles.css +1382 -0
  139. package/packages/web/src/ui/components/activityBar.js +35 -0
  140. package/packages/web/src/ui/components/activityPanel.js +132 -0
  141. package/packages/web/src/ui/components/commandPalette.js +154 -0
  142. package/packages/web/src/ui/components/editorArea.js +75 -0
  143. package/packages/web/src/ui/components/launcher.js +392 -0
  144. package/packages/web/src/ui/components/openers/index.js +212 -0
  145. package/packages/web/src/ui/components/openers/markdown.js +222 -0
  146. package/packages/web/src/ui/components/overlays.js +255 -0
  147. package/packages/web/src/ui/components/phoneChrome.js +188 -0
  148. package/packages/web/src/ui/components/pluginReview.js +151 -0
  149. package/packages/web/src/ui/components/pluginsView.js +120 -0
  150. package/packages/web/src/ui/components/settingsView.js +258 -0
  151. package/packages/web/src/ui/components/social.js +290 -0
  152. package/packages/web/src/ui/components/statusBar.js +198 -0
  153. package/packages/web/src/ui/components/views/grid.js +115 -0
  154. package/packages/web/src/ui/components/views/index.js +155 -0
  155. package/packages/web/src/ui/components/views/list.js +50 -0
  156. package/packages/web/src/ui/components/views/parts.js +58 -0
  157. package/packages/web/src/ui/compositions/workbench.js +125 -0
  158. package/packages/web/src/ui/format.js +33 -0
  159. package/packages/web/src/ui/icon.js +81 -0
  160. package/packages/web/src/ui/media.js +114 -0
  161. package/packages/web/src/ui/sanitize.js +86 -0
  162. package/packages/web/src/workbench.js +205 -0
@@ -0,0 +1,104 @@
1
+ // @3sln/trove/core — runtime-agnostic building blocks for a self-hostable drive.
2
+ // Import the pieces directly, or use `createVfs` to wire a sensible default.
3
+
4
+ export { TroveError, ErrorCode, wrapError, isRetryable, isOutOfSpace } from './errors.js';
5
+ export { withRetry, withTimeout } from './retry.js';
6
+ export * from './util.js';
7
+ export * from './links.js';
8
+
9
+ export { StorageBackend } from './storage/interface.js';
10
+ export { MemoryStorage } from './storage/memory.js';
11
+ export { FilesystemStorage } from './storage/filesystem.js';
12
+ export { S3Storage } from './storage/s3.js';
13
+ export { PrefixedStorage } from './storage/prefixed.js';
14
+
15
+ export { MetadataStore } from './metadata/interface.js';
16
+ export { MemoryStore } from './metadata/memory.js';
17
+ export { SqliteStore } from './metadata/sqlite.js';
18
+
19
+ export { CollectionService, CAPABILITIES } from './collections/index.js';
20
+
21
+ export { SearchService } from './search/index.js';
22
+ export { SearchTransformer, ParsingSearchTransformer, WorkersAiSearchTransformer, parseTagFilters, matchTagFilters } from './search/transformer.js';
23
+ export { VectorStore, MemoryVectorStore, QdrantVectorStore } from './search/vectorStore.js';
24
+ export { KeywordStore, MemoryKeywordStore } from './search/keywordStore.js';
25
+ export { EmbeddingProvider, LocalHashEmbedding, HttpEmbedding } from './search/embeddings.js';
26
+ // Durable local search: vectors via sqlite-vec, keywords via FTS5, both in the
27
+ // SQLite file the metadata already lives in.
28
+ export { SqliteVectorStore, SqliteKeywordStore, SEARCH_DB_KEY } from './search/sqliteStores.js';
29
+
30
+ export { IndexerRegistry, textIndexer, chunkText } from './indexers/registry.js';
31
+ export { PluginService, PackageStore, StoragePackageStore, PluginInstallStore, SqlitePluginInstallStore, MemoryPluginInstallStore, parsePluginPackage, capabilityList, ALL_CAPABILITIES, IndexerRuntime, InProcessIndexerRuntime, PluginIndexers, matchFromSelector } from './plugins/index.js';
32
+ export { UploadManager, DEFAULT_PART_SIZE } from './uploads.js';
33
+ export { Vfs, CONTENT_TYPES } from './vfs.js';
34
+ export { IndexingCoordinator } from './indexing.js';
35
+ // Work in flight (ephemeral) and standing problems (durable) — see the header of each.
36
+ export { TaskRegistry } from './tasks.js';
37
+ export { IssueRegistry, issueId } from './issues.js';
38
+ // Reconcile a collection against what its store actually holds (changes made outside Trove).
39
+ export { CollectionScanner } from './scan.js';
40
+ export { normalizeContribution, clampContribution, clampTagValue, DEFAULT_CAPS } from './indexers/contribution.js';
41
+
42
+ // Cloudflare Vectorize — first-class pluggable vector DB.
43
+ export { VectorizeVectorStore } from './search/vectorize.js';
44
+
45
+ // Server-side key/value store (subscriptions, inboxes, profiles).
46
+ export { KeyValueStore, MemoryKV, SqliteKV } from './kv.js';
47
+ export { SignedUrls, resolveUrlSecret, URL_PURPOSES } from './signedUrls.js';
48
+ export { SqliteDatabase, SqliteProvider, LocalSqliteProvider, assertSafePluginSql, stripSqlLiterals } from './sqlite.js';
49
+ // SQLite on Cloudflare D1, so a Worker deployment has a metadata store that exists.
50
+ export { D1SqliteProvider } from './sqlite-d1.js';
51
+
52
+ // Identity (BYO IdP — Cloudflare Access / Zero Trust / a proxy).
53
+ export {
54
+ IdentityProvider, JwtIdentityProvider, HeaderIdentityProvider,
55
+ AnonymousIdentityProvider, principalFromClaims,
56
+ cloudflareAccess, accessHost,
57
+ } from './identity/index.js';
58
+ export { verifyJwt, decodeJwt, JwksClient, StaticJwks } from './identity/jwt.js';
59
+ // Where an unauthenticated client is told to go — one answer for the whole drive.
60
+ export {
61
+ protectedResourceMetadata, challengeHeaders, metadataUrl, publicOrigin,
62
+ normalizeServers, resolveAuthDiscovery, usableAuthServer, headerSafe,
63
+ } from './identity/discovery.js';
64
+
65
+ // Sidecar documents: conversations, tags, indexer facets (CRDT, cold-in-S3).
66
+ export { SidecarService, SidecarStore, SidecarManager } from './sidecar/index.js';
67
+ export * as sidecarOps from './sidecar/document.js';
68
+
69
+ // Notifications: mention batching + web push.
70
+ export { NotificationCenter } from './notifications/index.js';
71
+ export { WebPushService, generateVapidKeys } from './notifications/webpush.js';
72
+
73
+ import { Vfs } from './vfs.js';
74
+ import { MemoryStorage } from './storage/memory.js';
75
+ import { MemoryStore } from './metadata/memory.js';
76
+ import { SearchService } from './search/index.js';
77
+ import { LocalHashEmbedding } from './search/embeddings.js';
78
+ import { IndexerRegistry } from './indexers/registry.js';
79
+
80
+ /**
81
+ * Wire a Vfs from parts, defaulting to in-memory everything (great for tests and
82
+ * a zero-config first run). Pass real backends for production:
83
+ *
84
+ * createVfs({
85
+ * storage: new S3Storage({ ... }),
86
+ * metadata: new SqliteStore({ provider: new LocalSqliteProvider({ path: 'trove.db' }) }),
87
+ * embeddings: new HttpEmbedding({ url, apiKey, model, dimensions: 1536 }),
88
+ * })
89
+ *
90
+ * @param {object} [opts]
91
+ * @returns {Promise<Vfs>} initialised (metadata.init already run)
92
+ */
93
+ export async function createVfs(opts = {}) {
94
+ const storage = opts.storage ?? new MemoryStorage();
95
+ const metadata = opts.metadata ?? new MemoryStore();
96
+ const embeddings = opts.embeddings ?? new LocalHashEmbedding();
97
+ const search =
98
+ opts.search ??
99
+ new SearchService({ embeddings, vectorStore: opts.vectorStore, keywordStore: opts.keywordStore });
100
+ const indexers = opts.indexers ?? new IndexerRegistry();
101
+ const vfs = new Vfs({ storage, metadata, search, indexers, sidecar: opts.sidecar, collections: opts.collections, searchTransformer: opts.searchTransformer, issues: opts.issues, maxIndexBytes: opts.maxIndexBytes, maxUploadBytes: opts.maxUploadBytes, uploadPartSize: opts.uploadPartSize });
102
+ await vfs.init();
103
+ return vfs;
104
+ }
@@ -0,0 +1,115 @@
1
+ // What a *contribution* is, and how big it may be.
2
+ //
3
+ // A contribution is the one shape everything that enriches an item produces — the
4
+ // built-in indexers, a plugin's server indexer running in the isolate, and a sandboxed
5
+ // plugin pushing through the API. It has up to three scopes:
6
+ //
7
+ // semanticTexts text chunks → embeddings + keyword search
8
+ // tags filterable key/values, merged into the item's queryable tags
9
+ // metadata arbitrary structured data (an audiobook's chapter index, …)
10
+ //
11
+ // Both halves of the contract live here because they must not drift: `normalize`
12
+ // decides what the shape IS, and `clamp` decides how large it may be. They used to sit
13
+ // in different modules — normalize in the indexing coordinator, clamp inside the plugin
14
+ // isolate runtime — which meant the size limits only applied to one of the three
15
+ // producers. The caps are a property of the contribution, not of whoever made it.
16
+
17
+ /** Output caps applied to every contribution, whatever produced it. */
18
+ export const DEFAULT_CAPS = {
19
+ maxSemanticTexts: 500, // number of chunks
20
+ maxTextChars: 100_000, // per chunk
21
+ maxSemanticChars: 2_000_000, // total across chunks
22
+ maxTags: 100, // number of tag entries
23
+ maxTagKeyChars: 128,
24
+ maxTagValueChars: 2_048,
25
+ maxMetadataBytes: 256 * 1024, // JSON-serialized metadata
26
+ };
27
+
28
+ /**
29
+ * Normalize to the three canonical scopes, accepting the legacy `{ documents, facet }`
30
+ * shape (documents→semanticTexts, facet→metadata).
31
+ */
32
+ export function normalizeContribution(c = {}) {
33
+ return {
34
+ semanticTexts: c.semanticTexts || c.documents || [],
35
+ tags: c.tags || null,
36
+ metadata: c.metadata || c.facet || null,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Clamp a contribution to `caps`, dropping (not erroring on) whatever exceeds them.
42
+ *
43
+ * Truncating rather than rejecting is deliberate: the input is a *derived* artifact of
44
+ * a file someone uploaded, so an oversized contribution usually means an unusual file
45
+ * rather than an attack, and losing the tail of an index is much better than losing the
46
+ * item's indexing entirely. What it must never do is let the size of one item's
47
+ * contribution be chosen by whoever wrote the file.
48
+ */
49
+ export function clampContribution(raw, caps = DEFAULT_CAPS) {
50
+ const c = { ...DEFAULT_CAPS, ...caps };
51
+ const out = {};
52
+ const src = raw && typeof raw === 'object' ? raw : {};
53
+
54
+ const texts = src.semanticTexts || src.documents;
55
+ if (Array.isArray(texts)) {
56
+ const docs = [];
57
+ let budget = c.maxSemanticChars;
58
+ for (const d of texts) {
59
+ if (docs.length >= c.maxSemanticTexts || budget <= 0) break;
60
+ const text = typeof d === 'string' ? d : (d && typeof d.text === 'string' ? d.text : null);
61
+ if (!text) continue;
62
+ const clipped = text.length > c.maxTextChars ? text.slice(0, c.maxTextChars) : text;
63
+ const room = Math.min(clipped.length, budget);
64
+ const finalText = room < clipped.length ? clipped.slice(0, room) : clipped;
65
+ budget -= finalText.length;
66
+ const doc = typeof d === 'string' ? { text: finalText } : { ...d, text: finalText };
67
+ if (doc.fields && typeof doc.fields !== 'object') delete doc.fields;
68
+ docs.push(doc);
69
+ }
70
+ if (docs.length) out.semanticTexts = docs;
71
+ }
72
+
73
+ const tags = src.tags;
74
+ if (tags && typeof tags === 'object' && !Array.isArray(tags)) {
75
+ const clean = {};
76
+ let n = 0;
77
+ for (const [k, v] of Object.entries(tags)) {
78
+ if (n >= c.maxTags) break;
79
+ if (typeof k !== 'string' || k.length > c.maxTagKeyChars) continue;
80
+ const cv = clampTagValue(v, c.maxTagValueChars);
81
+ if (cv === undefined) continue;
82
+ clean[k] = cv;
83
+ n++;
84
+ }
85
+ if (n) out.tags = clean;
86
+ }
87
+
88
+ const metadata = src.metadata || src.facet;
89
+ if (metadata && typeof metadata === 'object') {
90
+ try {
91
+ const json = JSON.stringify(metadata);
92
+ // BYTES, as the cap's name says. `json.length` counts UTF-16 code units, so
93
+ // non-ASCII metadata could run to roughly three times the advertised limit.
94
+ if (json && new TextEncoder().encode(json).length <= c.maxMetadataBytes) {
95
+ out.metadata = JSON.parse(json);
96
+ }
97
+ } catch { /* non-serializable metadata is dropped */ }
98
+ }
99
+
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * A tag value must be a filterable scalar. Objects and arrays are dropped — they can't
105
+ * be compared by a `#key:>=value` filter, so storing them would only bloat the row.
106
+ * `null` survives: it is the documented "this tag is removed" sentinel that
107
+ * mergeContributionTags reads, so clamping it away would make a contribution unable to
108
+ * retract a tag it previously set.
109
+ */
110
+ export function clampTagValue(v, maxChars) {
111
+ if (typeof v === 'string') return v.length > maxChars ? v.slice(0, maxChars) : v;
112
+ if (typeof v === 'number') return Number.isFinite(v) ? v : undefined; // NaN/∞ don't compare
113
+ if (typeof v === 'boolean' || v === null) return v;
114
+ return undefined;
115
+ }
@@ -0,0 +1,162 @@
1
+ // IndexerRegistry — server-side indexers that extract structured content from
2
+ // files. An indexer declares which nodes it handles and, for each, produces a
3
+ // *contribution* with up to three scopes:
4
+ // - semanticTexts: text chunks that become embeddings + keyword entries (search)
5
+ // - tags: filterable key/values (merged into the node's queryable tags)
6
+ // - metadata: arbitrary structured data (e.g. an audiobook's chapter index)
7
+ // Everything is namespaced under the indexer's `id`, so indexers never clobber one
8
+ // another and an indexer's whole contribution can be removed independently.
9
+ //
10
+ // This complements *plugin* indexers, which run in the browser sandbox and push
11
+ // the same contribution shape through the API under their own namespace.
12
+ //
13
+ // Two built-ins ship here: a text/markdown extractor (the reference indexer) and a
14
+ // links extractor, which is what makes a drive with no folders navigable.
15
+
16
+ import { extname } from '../util.js';
17
+ import { extractTroveLinks } from '../links.js';
18
+ import { LINKS_CONTRIBUTOR, LINKS_KEY } from '../metadata/interface.js';
19
+
20
+ /**
21
+ * @typedef {object} Indexer
22
+ * @property {string} id unique namespace
23
+ * @property {string} [displayName]
24
+ * @property {(node: object) => boolean} match
25
+ * @property {(node: object, ctx: IndexContext) => Promise<Contribution>} index
26
+ *
27
+ * @typedef {object} Contribution
28
+ * @property {Doc[]} [semanticTexts] text chunks → embeddings + keyword search
29
+ * @property {object} [tags] filterable key/values, e.g. { language: 'en' }
30
+ * @property {object} [metadata] arbitrary structured data, e.g. { chapters: [...] }
31
+ *
32
+ * @typedef {object} Doc
33
+ * @property {string} [id] stable per (node, chunk); defaults to `${node.id}:${i}`
34
+ * @property {string} text
35
+ * @property {object} [fields] e.g. { title, page, chapter }
36
+ *
37
+ * @typedef {object} IndexContext
38
+ * @property {() => Promise<Uint8Array>} readBytes
39
+ * @property {() => Promise<string>} readText
40
+ * @property {number} maxBytes
41
+ */
42
+
43
+ export class IndexerRegistry {
44
+ /** @param {{builtins?: boolean}} [opts] register the reference text indexer (default on) */
45
+ constructor({ builtins = true } = {}) {
46
+ this.indexers = new Map();
47
+ if (builtins) {
48
+ this.register(textIndexer);
49
+ this.register(linksIndexer);
50
+ }
51
+ }
52
+ register(indexer) {
53
+ if (!indexer?.id || typeof indexer.index !== 'function') {
54
+ throw new Error('Indexer requires an id and an index() function');
55
+ }
56
+ this.indexers.set(indexer.id, indexer);
57
+ // Only unregister if THIS registration is still the live one. Indexer ids are
58
+ // contribution URIs, which are account-independent, so a second account installing
59
+ // the same package replaces the first's entry — and the first's stale closure then
60
+ // deleted the second's, taking that account's indexer out of the drive-wide registry
61
+ // and purging its contributions.
62
+ return () => { if (this.indexers.get(indexer.id) === indexer) this.indexers.delete(indexer.id); };
63
+ }
64
+ unregister(id) {
65
+ this.indexers.delete(id);
66
+ }
67
+ list() {
68
+ return [...this.indexers.values()].map((i) => ({ id: i.id, displayName: i.displayName || i.id }));
69
+ }
70
+ matching(node) {
71
+ return [...this.indexers.values()].filter((i) => {
72
+ try {
73
+ return i.match(node);
74
+ } catch {
75
+ return false;
76
+ }
77
+ });
78
+ }
79
+ }
80
+
81
+ // --- Reference indexer: plain text / markdown / code ------------------------
82
+
83
+ const TEXT_EXTS = new Set([
84
+ '.txt', '.md', '.markdown', '.rst', '.log', '.csv', '.tsv',
85
+ '.json', '.yaml', '.yml', '.toml', '.ini', '.env',
86
+ '.js', '.mjs', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.rs', '.java',
87
+ '.c', '.h', '.cpp', '.css', '.html', '.xml', '.sh',
88
+ ]);
89
+
90
+ function isTexty(node) {
91
+ if (node.contentType?.startsWith('text/')) return true;
92
+ return TEXT_EXTS.has(extname(node.name));
93
+ }
94
+
95
+ export const textIndexer = {
96
+ id: 'core.text',
97
+ displayName: 'Text & code',
98
+ match: isTexty,
99
+ async index(node, ctx) {
100
+ const text = await ctx.readText();
101
+ if (!text.trim()) return { semanticTexts: [] };
102
+ const chunks = chunkText(text, 1200, 200);
103
+ // Fields describe the CHUNK (which chunk it is), not the node. The node's name
104
+ // deliberately isn't copied in: field values are indexed, so a name duplicated
105
+ // across every chunk goes stale the moment the item is renamed — leaving the file
106
+ // findable under a name it no longer has. Name search belongs to the `core.name`
107
+ // index, which is a single document and is refreshed on rename.
108
+ const semanticTexts = chunks.map((chunk, i) => ({
109
+ id: `${node.id}:${i}`,
110
+ text: chunk,
111
+ fields: { chunk: i },
112
+ }));
113
+ return {
114
+ semanticTexts,
115
+ metadata: { chars: text.length, chunks: chunks.length, excerpt: text.slice(0, 280) },
116
+ };
117
+ },
118
+ };
119
+
120
+ // --- Links indexer: what this item points at --------------------------------
121
+ //
122
+ // With no folder hierarchy, an item's place in the drive is defined by what links to
123
+ // it. This indexer records the outbound `trove:` URIs found in an item's text, which
124
+ // is what MetadataStore.findLinksTo reads to answer the inverse question — "what
125
+ // gathers this up?" — for the backlinks panel.
126
+ //
127
+ // It stores the links as `metadata`, not `semanticTexts`: a URI is an edge, not prose,
128
+ // and embedding it would only pollute semantic results. `links` is also surfaced as a
129
+ // tag count so `#links > 0` finds the documents that act as indexes.
130
+
131
+ export const linksIndexer = {
132
+ id: LINKS_CONTRIBUTOR,
133
+ displayName: 'Links',
134
+ match: isTexty,
135
+ async index(node, ctx) {
136
+ const text = await ctx.readText();
137
+ const links = extractTroveLinks(text).map((l) => l.uri);
138
+ return {
139
+ metadata: { [LINKS_KEY]: links },
140
+ tags: { links: links.length },
141
+ };
142
+ },
143
+ };
144
+
145
+ /** Split into overlapping windows on sentence/line boundaries where possible. */
146
+ export function chunkText(text, size, overlap) {
147
+ const out = [];
148
+ let i = 0;
149
+ while (i < text.length) {
150
+ let end = Math.min(i + size, text.length);
151
+ if (end < text.length) {
152
+ // Prefer to break at a paragraph/sentence boundary within the last 25%.
153
+ const window = text.slice(i, end);
154
+ const brk = Math.max(window.lastIndexOf('\n\n'), window.lastIndexOf('. '), window.lastIndexOf('\n'));
155
+ if (brk > size * 0.75) end = i + brk + 1;
156
+ }
157
+ out.push(text.slice(i, end).trim());
158
+ if (end >= text.length) break;
159
+ i = end - overlap;
160
+ }
161
+ return out.filter(Boolean);
162
+ }