contentbase 0.1.8 → 0.4.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 (40) hide show
  1. package/CNAME +1 -0
  2. package/LAUNCH-PLAN.md +419 -0
  3. package/README.md +90 -0
  4. package/index.html +1636 -0
  5. package/package.json +5 -4
  6. package/src/adapters/node-fs.ts +80 -0
  7. package/src/cli/commands/api/endpoints/doc.ts +2 -2
  8. package/src/cli/commands/api/endpoints/document.ts +2 -2
  9. package/src/cli/commands/api/endpoints/search-reindex.ts +6 -25
  10. package/src/cli/commands/api/endpoints/search-status.ts +3 -20
  11. package/src/cli/commands/api/endpoints/search.ts +3 -33
  12. package/src/cli/commands/api/endpoints/semantic-search.ts +3 -33
  13. package/src/cli/commands/embed.ts +8 -81
  14. package/src/cli/commands/mcp.ts +5 -1128
  15. package/src/cli/commands/search.ts +25 -90
  16. package/src/cli/commands/serve.ts +7 -32
  17. package/src/cli/index.ts +1 -1
  18. package/src/cli/load-collection.ts +2 -2
  19. package/src/collection.ts +42 -33
  20. package/src/document.ts +18 -0
  21. package/src/extract-sections.ts +3 -0
  22. package/src/index.ts +10 -0
  23. package/src/mcp/helpers.ts +7 -0
  24. package/src/mcp/model-info.ts +102 -0
  25. package/src/mcp/prompts.ts +199 -0
  26. package/src/mcp/readme.ts +153 -0
  27. package/src/mcp/resources.ts +89 -0
  28. package/src/mcp/server.ts +103 -0
  29. package/src/mcp/tools/mutation.ts +176 -0
  30. package/src/mcp/tools/query.ts +141 -0
  31. package/src/mcp/tools/search.ts +180 -0
  32. package/src/parse.ts +5 -0
  33. package/src/query/query-dsl.ts +1 -1
  34. package/src/relationships/has-many.ts +4 -2
  35. package/src/search/document-inputs.ts +85 -0
  36. package/src/search/luca-semantic-search.ts +75 -0
  37. package/src/types.ts +54 -0
  38. package/src/utils/index.ts +2 -0
  39. package/src/utils/match-pattern.ts +2 -2
  40. package/src/utils/strip-markdown.ts +59 -0
@@ -0,0 +1,75 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export type EmbeddingProvider = 'local' | 'openai'
5
+ export type ChunkStrategy = 'section' | 'fixed' | 'document'
6
+
7
+ export interface SemanticSearchConfig {
8
+ dbPath: string
9
+ embeddingProvider: EmbeddingProvider
10
+ embeddingModel: string
11
+ chunkStrategy: ChunkStrategy
12
+ chunkSize: number
13
+ chunkOverlap: number
14
+ }
15
+
16
+ export interface SemanticSearchOptions extends Partial<SemanticSearchConfig> {}
17
+
18
+ export const DEFAULT_SEMANTIC_SEARCH_OPTIONS = {
19
+ embeddingProvider: 'openai' as EmbeddingProvider,
20
+ embeddingModel: 'text-embedding-3-small',
21
+ chunkStrategy: 'section' as ChunkStrategy,
22
+ chunkSize: 900,
23
+ chunkOverlap: 0.15,
24
+ }
25
+
26
+ export function getSearchDbPath(rootPath: string): string {
27
+ return path.join(rootPath, '.contentbase/search.sqlite')
28
+ }
29
+
30
+ export function hasSearchIndex(rootPath: string): boolean {
31
+ const dbDir = path.join(rootPath, '.contentbase')
32
+ if (!existsSync(dbDir)) return false
33
+
34
+ try {
35
+ const files = readdirSync(dbDir) as string[]
36
+ return files.some((file) => file.startsWith('search.') && file.endsWith('.sqlite'))
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ export function buildSemanticSearchConfig(rootPath: string, options: SemanticSearchOptions = {}): SemanticSearchConfig {
43
+ return {
44
+ dbPath: options.dbPath ?? getSearchDbPath(rootPath),
45
+ embeddingProvider: options.embeddingProvider ?? DEFAULT_SEMANTIC_SEARCH_OPTIONS.embeddingProvider,
46
+ embeddingModel: options.embeddingModel ?? DEFAULT_SEMANTIC_SEARCH_OPTIONS.embeddingModel,
47
+ chunkStrategy: options.chunkStrategy ?? DEFAULT_SEMANTIC_SEARCH_OPTIONS.chunkStrategy,
48
+ chunkSize: options.chunkSize ?? DEFAULT_SEMANTIC_SEARCH_OPTIONS.chunkSize,
49
+ chunkOverlap: options.chunkOverlap ?? DEFAULT_SEMANTIC_SEARCH_OPTIONS.chunkOverlap,
50
+ }
51
+ }
52
+
53
+ export async function loadSemanticSearchClass(): Promise<any> {
54
+ const { SemanticSearch } = await import('luca/agi')
55
+ return SemanticSearch
56
+ }
57
+
58
+ export async function ensureSemanticSearchAttached(container: any, SemanticSearchClass?: any): Promise<any> {
59
+ const SemanticSearch = SemanticSearchClass ?? await loadSemanticSearchClass()
60
+ if (!container.features.available.includes('semanticSearch')) {
61
+ ;(SemanticSearch as any).attach(container as any)
62
+ }
63
+ return SemanticSearch
64
+ }
65
+
66
+ export async function createSemanticSearch(container: any, rootPath: string, options: SemanticSearchOptions = {}): Promise<any> {
67
+ await ensureSemanticSearchAttached(container)
68
+ return container.feature('semanticSearch', buildSemanticSearchConfig(rootPath, options))
69
+ }
70
+
71
+ export async function getInitializedSemanticSearch(container: any, rootPath: string, options: SemanticSearchOptions = {}): Promise<any> {
72
+ const semanticSearch = await createSemanticSearch(container, rootPath, options)
73
+ await semanticSearch.initDb()
74
+ return semanticSearch
75
+ }
package/src/types.ts CHANGED
@@ -15,6 +15,54 @@ export interface CollectionItem {
15
15
  size: number;
16
16
  }
17
17
 
18
+ // ─── Storage adapter ───
19
+
20
+ /** File metadata returned by stat and listFiles */
21
+ export interface FileStat {
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ size: number;
25
+ }
26
+
27
+ /** A file entry returned from listFiles — key plus pre-fetched stat */
28
+ export interface FileEntry {
29
+ key: string;
30
+ stat: FileStat;
31
+ }
32
+
33
+ /**
34
+ * Abstracts file system operations so Collections can run on top of
35
+ * any storage backend (local disk, Cloudflare R2, S3, etc.).
36
+ *
37
+ * Implement this interface and pass it as `adapter` in CollectionOptions.
38
+ */
39
+ export interface StorageAdapter {
40
+ /** List all file keys under rootPath whose names match the extension pattern.
41
+ * Returns key + pre-fetched stat so load() avoids per-file round-trips. */
42
+ listFiles(rootPath: string, match: RegExp): Promise<FileEntry[]>;
43
+
44
+ /** Read file content as a UTF-8 string */
45
+ readFile(key: string): Promise<string>;
46
+
47
+ /** Fetch metadata for a single key */
48
+ stat(key: string): Promise<FileStat>;
49
+
50
+ /** Write content to key, creating any parent directories/prefixes as needed */
51
+ writeFile(key: string, content: string): Promise<void>;
52
+
53
+ /** Delete the file at key (silently succeeds if the key does not exist) */
54
+ deleteFile(key: string): Promise<void>;
55
+
56
+ /** Join a root and path segments into a full key (e.g. path.resolve for local FS) */
57
+ join(root: string, ...parts: string[]): string;
58
+
59
+ /** Strip the root prefix from a full key to get a relative key */
60
+ relative(root: string, key: string): string;
61
+
62
+ /** Return the file extension including the leading dot (e.g. ".md") */
63
+ extname(key: string): string;
64
+ }
65
+
18
66
  /** Options when constructing a Collection */
19
67
  export interface CollectionOptions {
20
68
  rootPath: string;
@@ -24,6 +72,12 @@ export interface CollectionOptions {
24
72
  autoDiscover?: boolean;
25
73
  /** Optional custom module loader for models.ts discovery. When provided, used instead of native import(). Enables VM-based loading in compiled binaries where node_modules aren't available. */
26
74
  moduleLoader?: (filePath: string) => Record<string, any> | Promise<Record<string, any>>;
75
+ /**
76
+ * Storage adapter for file system operations.
77
+ * Defaults to NodeStorageAdapter (local disk via fs/promises).
78
+ * Swap in a custom adapter to use Cloudflare R2, S3, or any other backend.
79
+ */
80
+ adapter?: StorageAdapter;
27
81
  }
28
82
 
29
83
  // ─── Section system ───
@@ -10,3 +10,5 @@ export { parseTable } from "./parse-table";
10
10
  export { normalizeHeadings } from "./normalize-headings";
11
11
  export { readDirectory } from "./read-directory";
12
12
  export { matchPattern, matchPatterns } from "./match-pattern";
13
+ export { stripMarkdown } from "./strip-markdown";
14
+ export type { StripMarkdownOptions } from "./strip-markdown";
@@ -31,8 +31,8 @@ export function matchPattern(
31
31
  const params: Record<string, string> = {};
32
32
 
33
33
  for (let i = 0; i < patternParts.length; i++) {
34
- const pat = patternParts[i];
35
- const val = pathParts[i];
34
+ const pat = patternParts[i]!;
35
+ const val = pathParts[i]!;
36
36
 
37
37
  if (pat.startsWith(":")) {
38
38
  params[pat.slice(1)] = val;
@@ -0,0 +1,59 @@
1
+ import { toString } from "mdast-util-to-string";
2
+ import { toMarkdown } from "mdast-util-to-markdown";
3
+ import type { Root, RootContent, List } from "mdast";
4
+
5
+ export interface StripMarkdownOptions {
6
+ preserveLists?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Strip markdown syntax from an MDAST, returning plain text.
11
+ * Headings, paragraphs, code blocks, blockquotes, etc. become plain text.
12
+ * With preserveLists: true, list items retain their bullet/number formatting.
13
+ */
14
+ export function stripMarkdown(
15
+ ast: Root,
16
+ options: StripMarkdownOptions = {}
17
+ ): string {
18
+ const { preserveLists = false } = options;
19
+
20
+ return ast.children
21
+ .map((node: RootContent) => {
22
+ if (preserveLists && (node.type === "list")) {
23
+ return renderList(node as List, 0).trimEnd();
24
+ }
25
+ return toString(node);
26
+ })
27
+ .filter(Boolean)
28
+ .join("\n\n");
29
+ }
30
+
31
+ function renderList(list: List, depth: number): string {
32
+ const indent = " ".repeat(depth);
33
+ let index = list.start ?? 1;
34
+
35
+ return list.children
36
+ .map((item) => {
37
+ const bullet = list.ordered ? `${index++}.` : "-";
38
+ const lines: string[] = [];
39
+
40
+ let firstContent = true;
41
+ for (const child of item.children) {
42
+ if (child.type === "list") {
43
+ lines.push(renderList(child as List, depth + 1));
44
+ } else {
45
+ const text = toString(child);
46
+ if (!text) continue;
47
+ if (firstContent) {
48
+ lines.push(`${indent}${bullet} ${text}`);
49
+ firstContent = false;
50
+ } else {
51
+ lines.push(`${indent} ${text}`);
52
+ }
53
+ }
54
+ }
55
+
56
+ return lines.join("\n");
57
+ })
58
+ .join("\n");
59
+ }