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
@@ -1,8 +1,9 @@
1
1
  import { z } from 'zod'
2
- import { existsSync, readdirSync } from 'node:fs'
3
2
  import path from 'node:path'
4
3
  import { commands } from '../registry.js'
5
4
  import { loadCollection } from '../load-collection.js'
5
+ import { collectDocumentInputs } from '../../search/document-inputs.js'
6
+ import { getInitializedSemanticSearch, hasSearchIndex } from '../../search/luca-semantic-search.js'
6
7
 
7
8
  const argsSchema = z.object({
8
9
  mode: z.enum(['hybrid', 'keyword', 'vector']).default('hybrid'),
@@ -15,31 +16,8 @@ const argsSchema = z.object({
15
16
  contentFolder: z.string().optional(),
16
17
  })
17
18
 
18
- function hasSearchIndex(rootPath: string): boolean {
19
- const dbDir = path.join(rootPath, '.contentbase')
20
- if (!existsSync(dbDir)) return false
21
- try {
22
- const files = readdirSync(dbDir) as string[]
23
- return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
24
- } catch {
25
- return false
26
- }
27
- }
28
-
29
- async function getSemanticSearch(container: any, rootPath: string) {
30
- const { SemanticSearch } = await import('@soederpop/luca/agi')
31
- if (!container.features.available.includes('semanticSearch')) {
32
- SemanticSearch.attach(container)
33
- }
34
-
35
- const dbPath = path.join(rootPath, '.contentbase/search.sqlite')
36
- const ss = container.feature('semanticSearch', { dbPath })
37
- await ss.initDb()
38
- return ss
39
- }
40
-
41
19
  async function buildIndex(container: any, collection: any) {
42
- const ss = await getSemanticSearch(container, collection.rootPath)
20
+ const ss = await getInitializedSemanticSearch(container, collection.rootPath)
43
21
  const docs = collectDocumentInputs(collection)
44
22
 
45
23
  const toIndex = docs.filter((doc: any) => ss.needsReindex(doc))
@@ -61,56 +39,6 @@ async function buildIndex(container: any, collection: any) {
61
39
  return ss
62
40
  }
63
41
 
64
- function collectDocumentInputs(collection: any) {
65
- const inputs: any[] = []
66
- for (const pathId of collection.available) {
67
- const doc = collection.document(pathId)
68
- const modelDef = collection.findModelDefinition(pathId)
69
-
70
- const sections: any[] = []
71
- const lines = (doc.content as string).split('\n')
72
- let currentHeading: string | null = null
73
- let currentContent: string[] = []
74
-
75
- for (const line of lines) {
76
- const h2Match = line.match(/^## (.+)/)
77
- if (h2Match) {
78
- if (currentHeading) {
79
- sections.push({
80
- heading: currentHeading,
81
- headingPath: currentHeading,
82
- content: currentContent.join('\n').trim(),
83
- level: 2,
84
- })
85
- }
86
- currentHeading = h2Match[1].trim()
87
- currentContent = []
88
- } else if (currentHeading) {
89
- currentContent.push(line)
90
- }
91
- }
92
- if (currentHeading) {
93
- sections.push({
94
- heading: currentHeading,
95
- headingPath: currentHeading,
96
- content: currentContent.join('\n').trim(),
97
- level: 2,
98
- })
99
- }
100
-
101
- inputs.push({
102
- pathId,
103
- model: modelDef?.name ?? undefined,
104
- title: doc.title,
105
- slug: doc.slug,
106
- meta: doc.meta,
107
- content: doc.content,
108
- sections: sections.length > 0 ? sections : undefined,
109
- })
110
- }
111
- return inputs
112
- }
113
-
114
42
  async function handler(options: z.infer<typeof argsSchema>, { container }: { container: any }) {
115
43
  const ui = container.feature('ui')
116
44
  const query = container.argv._[1] as string | undefined
@@ -140,7 +68,7 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
140
68
  if (options.bootstrap && !hasSearchIndex(collection.rootPath)) {
141
69
  ss = await buildIndex(container, collection)
142
70
  } else {
143
- ss = await getSemanticSearch(container, collection.rootPath)
71
+ ss = await getInitializedSemanticSearch(container, collection.rootPath)
144
72
  }
145
73
 
146
74
  // Parse where clause: "key=value,key2=value2"
@@ -193,22 +121,29 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
193
121
  return
194
122
  }
195
123
 
196
- const header = `| # | Score | Document | Match |`
197
- const separator = `|---|-------|----------|-------|`
198
- const rows = results.map((r: any, i: number) => {
199
- const score = `${Math.round(r.score * 100)}%`
200
- const doc = r.title || r.pathId
201
- let match = ''
124
+ const colors = ui.colors
125
+ const cols = process.stdout.columns || 80
126
+ const pad = ' '
127
+ const maxSnippet = cols - pad.length - 2
128
+
129
+ for (let i = 0; i < results.length; i++) {
130
+ const r = results[i]
131
+ const title = r.title || r.pathId
132
+ console.log(`${colors.dim(`${i + 1}.`)} ${colors.bold(title)}`)
133
+ let relPath = r.pathId
134
+ try { relPath = path.relative(process.cwd(), collection.document(r.pathId).path) } catch {}
135
+ console.log(`${pad}${colors.cyan(relPath)}`)
202
136
  if (r.snippet) {
203
- const snippet = r.snippet.replace(/\n/g, ' ').replace(/\|/g, '\\|').substring(0, 80)
204
- const section = r.matchedSection || ''
205
- match = section ? `**${section}**: ${snippet}` : snippet
137
+ let snippet = r.snippet
138
+ .replace(/>>>/g, '').replace(/<<</g, '')
139
+ .replace(/\n/g, ' ').replace(/\s+/g, ' ')
140
+ .replace(/[`*_~\[\]]/g, '').trim()
141
+ if (r.matchedSection) snippet = `${r.matchedSection} — ${snippet}`
142
+ if (snippet.length > maxSnippet) snippet = snippet.substring(0, maxSnippet - 1) + '…'
143
+ console.log(`${pad}${colors.dim(snippet)}`)
206
144
  }
207
- return `| ${i + 1} | ${score} | ${doc} | ${match} |`
208
- })
209
-
210
- const table = [header, separator, ...rows].join('\n')
211
- ui.markdown(table)
145
+ if (i < results.length - 1) console.log()
146
+ }
212
147
 
213
148
  if (options.full) {
214
149
  for (const r of results) {
@@ -5,6 +5,8 @@ import { commands } from '../registry.js'
5
5
  import { loadCollection } from '../load-collection.js'
6
6
  import { buildSchemaJSON } from './api/helpers.js'
7
7
  import { builtinEndpoints } from './api/endpoints/index.js'
8
+ import { collectDocumentInputs } from '../../search/document-inputs.js'
9
+ import { getInitializedSemanticSearch, hasSearchIndex } from '../../search/luca-semantic-search.js'
8
10
 
9
11
  const argsSchema = z.object({
10
12
  port: z.number().default(8000),
@@ -184,39 +186,12 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
184
186
  // Search index auto-update
185
187
  // ---------------------------------------------------------------------------
186
188
  if (options.search) {
187
- const { existsSync, readdirSync } = await import('node:fs')
188
- const dbDir = path.join(collection.rootPath, '.contentbase')
189
- const hasIndex = existsSync(dbDir) && (() => {
189
+ if (hasSearchIndex(collection.rootPath)) {
190
190
  try {
191
- return (readdirSync(dbDir) as string[]).some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
192
- } catch { return false }
193
- })()
194
-
195
- if (hasIndex) {
196
- try {
197
- const { SemanticSearch } = await import('@soederpop/luca/agi')
198
- if (!container.features.available.includes('semanticSearch')) {
199
- SemanticSearch.attach(container)
200
- }
201
- const dbPath = path.join(collection.rootPath, '.contentbase/search.sqlite')
202
- const ss = container.feature('semanticSearch', { dbPath }) as any
203
- await ss.initDb()
204
-
205
- // Collect document inputs
206
- const docs: any[] = []
207
- for (const pathId of collection.available) {
208
- const doc = collection.document(pathId) as any
209
- docs.push({
210
- pathId,
211
- model: collection.findModelDefinition(pathId)?.name ?? undefined,
212
- title: doc.title,
213
- meta: doc.meta,
214
- content: doc.content,
215
- })
216
- }
217
-
218
- const stale = docs.filter((d: any) => ss.needsReindex(d))
219
- ss.removeStale(docs.map((d: any) => d.pathId))
191
+ const ss = await getInitializedSemanticSearch(container, collection.rootPath)
192
+ const docs = collectDocumentInputs(collection)
193
+ const stale = docs.filter((d) => ss.needsReindex(d))
194
+ ss.removeStale(docs.map((d) => d.pathId))
220
195
 
221
196
  if (stale.length > 0) {
222
197
  console.log(`[search] Updating ${stale.length} stale document(s)...`)
package/src/cli/index.ts CHANGED
@@ -12,7 +12,7 @@ async function renderMarkdown(container: any, text: string) {
12
12
 
13
13
  async function main() {
14
14
  // Dynamic import so the library stays luca-free; only the CLI pulls it in
15
- const luca = await import('@soederpop/luca/node')
15
+ const luca = await import('luca/node')
16
16
  const container = luca.default
17
17
 
18
18
  const commandName = container.argv._[0] as string | undefined
@@ -138,10 +138,10 @@ export async function loadCollection(options: {
138
138
  const cwd = process.cwd();
139
139
 
140
140
  // If no container was passed, try to grab the luca singleton.
141
- // This works when running inside the cnotes CLI (which imports @soederpop/luca/node).
141
+ // This works when running inside the cnotes CLI (which imports luca/node).
142
142
  if (!container) {
143
143
  try {
144
- const luca = await import('@soederpop/luca/node');
144
+ const luca = await import('luca/node');
145
145
  container = luca.default;
146
146
  } catch {
147
147
  // Not running in a luca context — that's fine, native imports will be used
package/src/collection.ts CHANGED
@@ -1,13 +1,12 @@
1
- import fs from "fs/promises";
2
1
  import path from "path";
3
2
  import matter from "gray-matter";
4
3
  import picomatch from "picomatch";
5
4
  import { Document } from "./document";
6
5
  import { CollectionQuery } from "./query/collection-query";
7
6
  import { createModelInstance } from "./model-instance";
8
- import { readDirectory } from "./utils/read-directory";
9
7
  import { pluralize } from "./utils/inflect";
10
8
  import { Base } from "./base-model";
9
+ import { NodeStorageAdapter } from "./adapters/node-fs";
11
10
  import type {
12
11
  ModelDefinition,
13
12
  CollectionItem,
@@ -15,6 +14,7 @@ import type {
15
14
  InferModelInstance,
16
15
  HasManyDefinition,
17
16
  RelationshipDefinition,
17
+ StorageAdapter,
18
18
  } from "./types";
19
19
 
20
20
  // ─── Zod schema introspection ───
@@ -110,6 +110,7 @@ export class Collection {
110
110
  readonly name: string;
111
111
  readonly extensions: string[];
112
112
 
113
+ #adapter: StorageAdapter;
113
114
  #items: Map<string, CollectionItem> = new Map();
114
115
  #documents: Map<string, Document> = new Map();
115
116
  #models: Map<string, ModelDefinition<any, any, any, any, any>> = new Map();
@@ -120,7 +121,13 @@ export class Collection {
120
121
  #moduleLoader?: (filePath: string) => Record<string, any> | Promise<Record<string, any>>;
121
122
 
122
123
  constructor(options: CollectionOptions) {
123
- this.rootPath = path.resolve(options.rootPath);
124
+ this.#adapter = options.adapter ?? new NodeStorageAdapter();
125
+ // Resolve to an absolute path for local FS (NodeStorageAdapter default).
126
+ // When a custom adapter is provided, rootPath is the backend-specific root
127
+ // (e.g. a bucket prefix for R2) and is stored as-is.
128
+ this.rootPath = options.adapter
129
+ ? options.rootPath
130
+ : path.resolve(options.rootPath);
124
131
  this.name = options.name ?? options.rootPath;
125
132
  this.extensions = options.extensions ?? ["mdx", "md"];
126
133
  this.#autoDiscover = options.autoDiscover ?? true;
@@ -191,9 +198,17 @@ export class Collection {
191
198
  }
192
199
 
193
200
  get available(): string[] {
194
- return Array.from(this.#items.keys()).filter(
195
- (id) => !this.#isExcludedByModel(id)
196
- );
201
+ return Array.from(this.#items.keys())
202
+ .filter((id) => !this.#isExcludedByModel(id))
203
+ .sort();
204
+ }
205
+
206
+ /**
207
+ * Match available pathIds against one or more glob patterns.
208
+ */
209
+ matchPaths(patterns: string | string[]): string[] {
210
+ const isMatch = picomatch(patterns);
211
+ return this.available.filter((id) => isMatch(id));
197
212
  }
198
213
 
199
214
  /**
@@ -244,27 +259,26 @@ export class Collection {
244
259
  `\\.(${this.extensions.join("|")})$`,
245
260
  "i"
246
261
  );
247
- const paths = await readDirectory(this.rootPath, extensionPattern);
262
+ const entries = await this.#adapter.listFiles(this.rootPath, extensionPattern);
248
263
 
249
264
  await Promise.all(
250
- paths.map(async (filePath) => {
251
- const pathId = this.getPathId(filePath);
265
+ entries.map(async ({ key, stat }) => {
266
+ const pathId = this.getPathId(key);
252
267
 
253
268
  // Globally exclude templates directory — templates are only used
254
269
  // for scaffolding and should never appear in queries or listings
255
270
  if (pathId.startsWith("templates/") || pathId === "templates") return;
256
271
 
257
- const raw = await fs.readFile(filePath, "utf8");
258
- const stat = await fs.stat(filePath);
272
+ const raw = await this.#adapter.readFile(key);
259
273
  const { data, content } = matter(raw);
260
274
 
261
275
  this.#items.set(pathId, {
262
276
  raw,
263
277
  content,
264
278
  meta: data,
265
- path: filePath,
266
- createdAt: stat.ctime,
267
- updatedAt: stat.mtime,
279
+ path: key,
280
+ createdAt: stat.createdAt,
281
+ updatedAt: stat.updatedAt,
268
282
  size: stat.size,
269
283
  });
270
284
  })
@@ -440,8 +454,7 @@ export class Collection {
440
454
  const item = this.#items.get(pathId)!;
441
455
  const filePath = item.path;
442
456
 
443
- await fs.mkdir(path.parse(filePath).dir, { recursive: true });
444
- await fs.writeFile(filePath, options.content, "utf8");
457
+ await this.#adapter.writeFile(filePath, options.content);
445
458
 
446
459
  // Update the stored item
447
460
  item.raw = options.content;
@@ -460,11 +473,7 @@ export class Collection {
460
473
  const item = this.#items.get(pathId);
461
474
  if (!item) return this;
462
475
 
463
- try {
464
- await fs.rm(item.path);
465
- } catch {
466
- // File might not exist
467
- }
476
+ await this.#adapter.deleteFile(item.path);
468
477
  this.#items.delete(pathId);
469
478
  this.#documents.delete(pathId);
470
479
  return this;
@@ -482,8 +491,8 @@ export class Collection {
482
491
  filePath = this.resolve(`${pathId}.${extension}`);
483
492
  }
484
493
 
485
- const raw = await fs.readFile(filePath, "utf8");
486
- const stat = await fs.stat(filePath);
494
+ const raw = await this.#adapter.readFile(filePath);
495
+ const stat = await this.#adapter.stat(filePath);
487
496
  const { data, content } = matter(raw);
488
497
 
489
498
  const item: CollectionItem = {
@@ -491,8 +500,8 @@ export class Collection {
491
500
  content,
492
501
  meta: data,
493
502
  path: filePath,
494
- createdAt: stat.ctime,
495
- updatedAt: stat.mtime,
503
+ createdAt: stat.createdAt,
504
+ updatedAt: stat.updatedAt,
496
505
  size: stat.size,
497
506
  };
498
507
 
@@ -542,11 +551,11 @@ export class Collection {
542
551
  // ─── Utilities ───
543
552
 
544
553
  resolve(...args: string[]): string {
545
- return path.resolve(this.rootPath, ...args);
554
+ return this.#adapter.join(this.rootPath, ...args);
546
555
  }
547
556
 
548
- getPathId(absolutePath: string): string {
549
- const relativePath = path.relative(this.rootPath, absolutePath);
557
+ getPathId(key: string): string {
558
+ const relativePath = this.#adapter.relative(this.rootPath, key);
550
559
  return relativePath.replace(/\.[a-z]+$/i, "");
551
560
  }
552
561
 
@@ -669,7 +678,7 @@ export class Collection {
669
678
 
670
679
  for (const pathId of sorted) {
671
680
  const item = this.#items.get(pathId)!;
672
- const ext = path.extname(item.path);
681
+ const ext = this.#adapter.extname(item.path);
673
682
  const fullPath = `${pathId}${ext}`;
674
683
  const parts = fullPath.split("/");
675
684
 
@@ -703,7 +712,7 @@ export class Collection {
703
712
 
704
713
  #tocEntry(pathId: string, basePath: string): string {
705
714
  const item = this.#items.get(pathId)!;
706
- const ext = path.extname(item.path);
715
+ const ext = this.#adapter.extname(item.path);
707
716
  const relativePath = `${basePath}/${pathId}${ext}`;
708
717
  const doc = this.document(pathId);
709
718
  return `- [${doc.title}](${relativePath})`;
@@ -774,12 +783,12 @@ export class Collection {
774
783
  */
775
784
  async saveModelSummary(options: { includeIds?: boolean } = {}): Promise<string> {
776
785
  const summary = this.generateModelSummary(options);
777
- const modelsPath = path.join(this.rootPath, "README.md");
786
+ const modelsPath = this.#adapter.join(this.rootPath, "README.md");
778
787
 
779
788
  // Preserve existing Overview section content
780
789
  let overview = "";
781
790
  try {
782
- const existing = await fs.readFile(modelsPath, "utf8");
791
+ const existing = await this.#adapter.readFile(modelsPath);
783
792
  const overviewStart = existing.indexOf("## Overview");
784
793
  if (overviewStart !== -1) {
785
794
  const contentStart = existing.indexOf("\n", overviewStart) + 1;
@@ -807,7 +816,7 @@ export class Collection {
807
816
  ];
808
817
 
809
818
  const markdown = lines.join("\n");
810
- await fs.writeFile(modelsPath, markdown, "utf8");
819
+ await this.#adapter.writeFile(modelsPath, markdown);
811
820
  return summary;
812
821
  }
813
822
 
package/src/document.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { unified } from "unified";
2
2
  import remarkParse from "remark-parse";
3
3
  import remarkGfm from "remark-gfm";
4
+ import remarkRehype from "remark-rehype";
5
+ import rehypeStringify from "rehype-stringify";
4
6
  import yaml from "js-yaml";
5
7
  import { toString } from "mdast-util-to-string";
6
8
  import { kebabCase } from "./utils/inflect";
@@ -9,6 +11,8 @@ import { NodeShortcuts } from "./node-shortcuts";
9
11
  import { stringifyAst } from "./utils/stringify-ast";
10
12
  import { normalizeHeadings } from "./utils/normalize-headings";
11
13
  import { parseTable } from "./utils/parse-table";
14
+ import { stripMarkdown } from "./utils/strip-markdown";
15
+ import type { StripMarkdownOptions } from "./utils/strip-markdown";
12
16
  import type { Root, Content, RootContent, Heading } from "mdast";
13
17
  import type { Collection } from "./collection";
14
18
 
@@ -443,6 +447,10 @@ export class Document {
443
447
  };
444
448
  }
445
449
 
450
+ stripMarkdown(options?: StripMarkdownOptions): string {
451
+ return stripMarkdown(this.ast, options);
452
+ }
453
+
446
454
  toText(
447
455
  filterFn: (node: Content) => boolean = () => true
448
456
  ): string {
@@ -457,6 +465,16 @@ export class Document {
457
465
  * Each heading is indented based on its depth relative to the
458
466
  * minimum heading depth found in the document.
459
467
  */
468
+ async toHtml(): Promise<string> {
469
+ const result = await unified()
470
+ .use(remarkParse)
471
+ .use(remarkGfm)
472
+ .use(remarkRehype)
473
+ .use(rehypeStringify)
474
+ .process(this.content);
475
+ return String(result);
476
+ }
477
+
460
478
  toOutline(): string {
461
479
  const headings = (this.ast.children as Content[]).filter(
462
480
  (n): n is Heading => n.type === "heading"
@@ -2,6 +2,8 @@ import { toString } from "mdast-util-to-string";
2
2
  import { AstQuery } from "./ast-query";
3
3
  import { NodeShortcuts } from "./node-shortcuts";
4
4
  import { stringifyAst } from "./utils/stringify-ast";
5
+ import { stripMarkdown } from "./utils/strip-markdown";
6
+ import type { StripMarkdownOptions } from "./utils/strip-markdown";
5
7
  import type { Root, Content, RootContent, Heading } from "mdast";
6
8
  import type { ParsedDocument } from "./parse";
7
9
 
@@ -150,6 +152,7 @@ function buildParsedDocument(ast: Root): ParsedDocument {
150
152
  nodes,
151
153
  title,
152
154
  stringify: (tree: Root = ast) => stringifyAst(tree),
155
+ stripMarkdown: (options?: StripMarkdownOptions) => stripMarkdown(ast, options),
153
156
  extractSection,
154
157
  querySection,
155
158
  };
package/src/index.ts CHANGED
@@ -39,8 +39,15 @@ export { validateDocument } from "./validator";
39
39
  // Pattern matching
40
40
  export { matchPattern, matchPatterns } from "./utils/match-pattern";
41
41
 
42
+ // Markdown utilities
43
+ export { stripMarkdown } from "./utils/strip-markdown";
44
+ export type { StripMarkdownOptions } from "./utils/strip-markdown";
45
+
42
46
  import { toString } from "mdast-util-to-string";
43
47
 
48
+ // Storage adapters
49
+ export { NodeStorageAdapter } from "./adapters/node-fs";
50
+
44
51
  // Types
45
52
  export type {
46
53
  ModelDefinition,
@@ -57,6 +64,9 @@ export type {
57
64
  SerializeOptions,
58
65
  SaveOptions,
59
66
  DocumentRef,
67
+ StorageAdapter,
68
+ FileStat,
69
+ FileEntry,
60
70
  } from "./types";
61
71
 
62
72
  // Re-export zod for convenience
@@ -0,0 +1,7 @@
1
+ export function errorResult(message: string) {
2
+ return { content: [{ type: 'text' as const, text: message }], isError: true }
3
+ }
4
+
5
+ export function textResult(text: string) {
6
+ return { content: [{ type: 'text' as const, text }] }
7
+ }
@@ -0,0 +1,102 @@
1
+ import { introspectMetaSchema } from '../index.js'
2
+
3
+ export function generateModelInfo(collection: any, def: any) {
4
+ const lines: string[] = []
5
+ const name = def.name as string
6
+ const prefix = def.prefix as string
7
+ const description = def.description || ''
8
+ const prefixDocs = collection.available.filter((id: string) => id.startsWith(prefix + '/'))
9
+
10
+ lines.push(`# Model: ${name}`, '')
11
+ lines.push(`- **Prefix:** \`${prefix}/\``)
12
+ lines.push(`- **Documents:** ${prefixDocs.length}`)
13
+ if (description) lines.push(`- **Description:** ${description}`)
14
+ lines.push('')
15
+
16
+ // Fields
17
+ const fields = introspectMetaSchema(def.meta)
18
+ if (fields.length > 0) {
19
+ lines.push('## Frontmatter Fields', '')
20
+ lines.push('| Field | Type | Required | Default | Description |')
21
+ lines.push('|-------|------|----------|---------|-------------|')
22
+ for (const f of fields as any[]) {
23
+ const req = f.required ? 'yes' : 'no'
24
+ const def_val = f.defaultValue !== undefined ? `\`${JSON.stringify(f.defaultValue)}\`` : ''
25
+ const desc = f.description || ''
26
+ lines.push(`| ${f.name} | ${f.type} | ${req} | ${def_val} | ${desc} |`)
27
+ }
28
+ lines.push('')
29
+ }
30
+
31
+ // Sections
32
+ const sections = Object.entries(def.sections || {})
33
+ if (sections.length > 0) {
34
+ lines.push('## Sections', '')
35
+ for (const [key, sec] of sections as [string, any][]) {
36
+ lines.push(`- **${sec.heading}** (key: \`${key}\`)${sec.schema ? ' — has schema validation' : ''}`)
37
+ if (sec.alternatives?.length) {
38
+ lines.push(` Alternatives: ${sec.alternatives.join(', ')}`)
39
+ }
40
+ }
41
+ lines.push('')
42
+ }
43
+
44
+ // Relationships
45
+ const relationships = Object.entries(def.relationships || {})
46
+ if (relationships.length > 0) {
47
+ lines.push('## Relationships', '')
48
+ for (const [key, rel] of relationships as [string, any][]) {
49
+ lines.push(`- \`${key}\` → ${rel.type} **${rel.model}**`)
50
+ }
51
+ lines.push('')
52
+ }
53
+
54
+ // Computed & Scopes
55
+ const computedKeys = Object.keys(def.computed || {})
56
+ if (computedKeys.length > 0) {
57
+ lines.push('## Computed Properties', '')
58
+ lines.push(computedKeys.map(k => `- \`${k}\``).join('\n'))
59
+ lines.push('')
60
+ }
61
+
62
+ const scopeKeys = Object.keys(def.scopes || {})
63
+ if (scopeKeys.length > 0) {
64
+ lines.push('## Named Scopes', '')
65
+ lines.push(scopeKeys.map(k => `- \`${k}\``).join('\n'))
66
+ lines.push('')
67
+ }
68
+
69
+ // Existing documents
70
+ if (prefixDocs.length > 0) {
71
+ lines.push('## Existing Documents', '')
72
+ for (const id of prefixDocs) {
73
+ lines.push(`- \`${id}\``)
74
+ }
75
+ lines.push('')
76
+ }
77
+
78
+ // Example scaffold
79
+ const defaultMeta: Record<string, any> = {}
80
+ for (const f of fields as any[]) {
81
+ if (f.defaultValue !== undefined) {
82
+ defaultMeta[f.name] = f.defaultValue
83
+ } else if (f.required) {
84
+ defaultMeta[f.name] = `<${f.type}>`
85
+ }
86
+ }
87
+ const sectionHeadings = sections.map(([, sec]: [string, any]) => `## ${sec.heading}\n\n`)
88
+
89
+ lines.push('## Example Document', '')
90
+ lines.push('```markdown')
91
+ lines.push('---')
92
+ for (const [k, v] of Object.entries(defaultMeta)) {
93
+ lines.push(`${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
94
+ }
95
+ lines.push('---')
96
+ lines.push(`# Your Title Here`)
97
+ lines.push('')
98
+ lines.push(sectionHeadings.join(''))
99
+ lines.push('```')
100
+
101
+ return lines.join('\n')
102
+ }