contentbase 0.0.2 → 0.0.4

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 (85) hide show
  1. package/.mcp.json +9 -0
  2. package/CLI.md +593 -0
  3. package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
  4. package/MCP-SERVER-SPEC.md +453 -0
  5. package/PRIMER.md +540 -0
  6. package/README.md +289 -13
  7. package/bun.lock +43 -4
  8. package/dist/cnotes +0 -0
  9. package/docs/README.md +110 -0
  10. package/docs/TABLE-OF-CONTENTS.md +7 -0
  11. package/docs/models.ts +38 -0
  12. package/models.ts +38 -0
  13. package/package.json +12 -3
  14. package/public/web-demo/index.html +813 -0
  15. package/showcases/node_modules/.cache/.repl_history +3 -0
  16. package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
  17. package/src/__tests__/semantic-search.integration.test.ts +284 -0
  18. package/src/api/endpoints/actions.ts +34 -0
  19. package/src/api/endpoints/doc.ts +187 -0
  20. package/src/api/endpoints/docs-index.ts +26 -0
  21. package/src/api/endpoints/document.ts +171 -0
  22. package/src/api/endpoints/documents.ts +71 -0
  23. package/src/api/endpoints/inspect.ts +16 -0
  24. package/src/api/endpoints/models.ts +10 -0
  25. package/src/api/endpoints/query.ts +88 -0
  26. package/src/api/endpoints/root.ts +7 -0
  27. package/src/api/endpoints/search-reindex.ts +65 -0
  28. package/src/api/endpoints/search-status.ts +55 -0
  29. package/src/api/endpoints/search.ts +104 -0
  30. package/src/api/endpoints/semantic-search.ts +120 -0
  31. package/src/api/endpoints/text-search.ts +63 -0
  32. package/src/api/endpoints/validate.ts +34 -0
  33. package/src/api/helpers.ts +97 -0
  34. package/src/base-model.ts +12 -0
  35. package/src/cli/commands/action.ts +82 -44
  36. package/src/cli/commands/console.ts +124 -0
  37. package/src/cli/commands/create.ts +179 -53
  38. package/src/cli/commands/embed.ts +323 -0
  39. package/src/cli/commands/export.ts +58 -24
  40. package/src/cli/commands/extract.ts +174 -0
  41. package/src/cli/commands/help.ts +81 -0
  42. package/src/cli/commands/index.ts +17 -0
  43. package/src/cli/commands/init.ts +72 -48
  44. package/src/cli/commands/inspect.ts +56 -46
  45. package/src/cli/commands/mcp.ts +1219 -0
  46. package/src/cli/commands/search.ts +285 -0
  47. package/src/cli/commands/serve.ts +348 -0
  48. package/src/cli/commands/summary.ts +60 -0
  49. package/src/cli/commands/teach.ts +86 -0
  50. package/src/cli/commands/text-search.ts +134 -0
  51. package/src/cli/commands/validate.ts +126 -64
  52. package/src/cli/index.ts +88 -19
  53. package/src/cli/load-collection.ts +144 -17
  54. package/src/cli/registry.ts +28 -0
  55. package/src/collection.ts +361 -10
  56. package/src/define-model.ts +101 -4
  57. package/src/document.ts +47 -1
  58. package/src/extract-sections.ts +1 -1
  59. package/src/index.ts +14 -2
  60. package/src/model-instance.ts +35 -5
  61. package/src/node-shortcuts.ts +1 -1
  62. package/src/query/collection-query.ts +96 -9
  63. package/src/query/index.ts +7 -0
  64. package/src/query/query-dsl.ts +259 -0
  65. package/src/relationships/has-many.ts +39 -0
  66. package/src/relationships/index.ts +7 -2
  67. package/src/section.ts +2 -0
  68. package/src/types.ts +23 -1
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/match-pattern.ts +65 -0
  71. package/src/validator.ts +18 -1
  72. package/test/collection.test.ts +118 -2
  73. package/test/fixtures/sdlc/MODELS.md +106 -0
  74. package/test/fixtures/sdlc/SKILL.md +404 -0
  75. package/test/fixtures/sdlc/index.ts +9 -0
  76. package/test/fixtures/sdlc/models.ts +8 -6
  77. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
  78. package/test/fixtures/sdlc/templates/epic.md +23 -0
  79. package/test/fixtures/sdlc/templates/story.md +19 -0
  80. package/test/pattern.test.ts +191 -0
  81. package/test/query-dsl.test.ts +431 -0
  82. package/test/query.test.ts +29 -0
  83. package/test/relationships.test.ts +130 -0
  84. package/test/section.test.ts +61 -0
  85. package/test/table-of-contents.test.ts +49 -5
@@ -1,38 +1,132 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
3
  import { Collection } from "../collection";
4
+ import { defineModel } from "../define-model";
5
+ import { singularize, upperFirst } from "../utils/inflect";
6
+
7
+ /**
8
+ * Search for a file with the given basename and common extensions.
9
+ */
10
+ async function findFile(
11
+ dir: string,
12
+ basename: string
13
+ ): Promise<string | undefined> {
14
+ for (const ext of ["ts", "js", "mjs"]) {
15
+ const candidate = path.resolve(dir, `${basename}.${ext}`);
16
+ try {
17
+ await fs.stat(candidate);
18
+ return candidate;
19
+ } catch {
20
+ // Not found, try next
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /**
27
+ * Duck-type check: does this value look like a ModelDefinition?
28
+ */
29
+ function isModelDefinition(value: unknown): boolean {
30
+ return (
31
+ typeof value === "object" &&
32
+ value !== null &&
33
+ typeof (value as any).name === "string" &&
34
+ typeof (value as any).prefix === "string" &&
35
+ "meta" in (value as any)
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Check if a directory (up to 2 levels deep) contains .md or .mdx files.
41
+ */
42
+ async function directoryContainsMarkdown(dirPath: string): Promise<boolean> {
43
+ try {
44
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (
47
+ entry.isFile() &&
48
+ (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))
49
+ ) {
50
+ return true;
51
+ }
52
+ if (entry.isDirectory()) {
53
+ const subEntries = await fs.readdir(
54
+ path.join(dirPath, entry.name),
55
+ { withFileTypes: true }
56
+ );
57
+ for (const sub of subEntries) {
58
+ if (
59
+ sub.isFile() &&
60
+ (sub.name.endsWith(".md") || sub.name.endsWith(".mdx"))
61
+ ) {
62
+ return true;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ } catch {
68
+ // Directory unreadable
69
+ }
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Tier 3: Auto-discover models from top-level subdirectories containing markdown.
75
+ */
76
+ async function autoDiscoverModels(collection: Collection): Promise<number> {
77
+ const rootPath = collection.rootPath;
78
+ let registered = 0;
79
+
80
+ try {
81
+ const entries = await fs.readdir(rootPath, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ if (!entry.isDirectory()) continue;
84
+ const dirPath = path.join(rootPath, entry.name);
85
+ if (await directoryContainsMarkdown(dirPath)) {
86
+ const folderName = entry.name;
87
+ const modelName = upperFirst(singularize(folderName));
88
+ const model = defineModel(modelName, { prefix: folderName });
89
+ collection.register(model);
90
+ registered++;
91
+ }
92
+ }
93
+ } catch {
94
+ // Root path unreadable
95
+ }
96
+
97
+ return registered;
98
+ }
4
99
 
5
100
  export async function loadCollection(options: {
6
- rootPath?: string;
101
+ contentFolder?: string;
7
102
  modulePath?: string;
8
103
  }): Promise<Collection> {
9
- let { rootPath, modulePath } = options;
104
+ let { contentFolder, modulePath } = options;
105
+ let rootPath: string | undefined;
106
+
107
+ const cwd = process.cwd();
10
108
 
11
- if (!rootPath) {
12
- const cwd = process.cwd();
109
+ if (contentFolder) {
110
+ // Resolve relative to cwd
111
+ rootPath = path.resolve(cwd, contentFolder);
112
+ } else {
113
+ // Check package.json for configured content folder
13
114
  const pkgPath = path.resolve(cwd, "package.json");
14
115
  try {
15
116
  const manifest = JSON.parse(await fs.readFile(pkgPath, "utf8"));
16
- if (manifest.contentbase?.rootPath) {
17
- rootPath = path.resolve(cwd, manifest.contentbase.rootPath);
117
+ if (manifest.contentbase?.contentFolder) {
118
+ rootPath = path.resolve(cwd, manifest.contentbase.contentFolder);
18
119
  }
19
120
  } catch {
20
121
  // No package.json found
21
122
  }
22
- rootPath = rootPath ?? cwd;
123
+ // Default to ./docs
124
+ rootPath = rootPath ?? path.resolve(cwd, "docs");
23
125
  }
24
126
 
127
+ // Tier 1: index.ts — full collection with models already registered
25
128
  if (!modulePath) {
26
- for (const ext of ["ts", "js", "mjs"]) {
27
- const candidate = path.resolve(rootPath, `index.${ext}`);
28
- try {
29
- await fs.stat(candidate);
30
- modulePath = candidate;
31
- break;
32
- } catch {
33
- // Not found, try next
34
- }
35
- }
129
+ modulePath = await findFile(rootPath, "index");
36
130
  }
37
131
 
38
132
  if (modulePath) {
@@ -47,7 +141,40 @@ export async function loadCollection(options: {
47
141
  return collection;
48
142
  }
49
143
 
144
+ // Tier 2: models.ts — import model definitions and auto-register
145
+ const modelsPath = await findFile(rootPath, "models");
146
+
147
+ if (modelsPath) {
148
+ const mod = await import(modelsPath);
149
+ const collection = new Collection({ rootPath });
150
+
151
+ let registered = 0;
152
+ for (const [, value] of Object.entries(mod)) {
153
+ if (isModelDefinition(value)) {
154
+ collection.register(value as any);
155
+ registered++;
156
+ }
157
+ }
158
+
159
+
160
+ await collection.load();
161
+ return collection;
162
+ }
163
+
164
+ // Tier 3: Auto-discover models from folder structure
50
165
  const collection = new Collection({ rootPath });
166
+ const discovered = await autoDiscoverModels(collection);
167
+
168
+ if (discovered > 0) {
169
+ console.warn(
170
+ `[contentbase] Auto-discovered ${discovered} model(s) from folder structure. These models have no schema validation. Create a models.ts or index.ts for proper type definitions.`
171
+ );
172
+ } else {
173
+ console.warn(
174
+ `[contentbase] No models or markdown files found in ${rootPath}. Run 'cnotes init' to set up a project.`
175
+ );
176
+ }
177
+
51
178
  await collection.load();
52
179
  return collection;
53
180
  }
@@ -0,0 +1,28 @@
1
+ import type { z } from 'zod'
2
+
3
+ export interface CommandDefinition {
4
+ description: string
5
+ help?: string
6
+ argsSchema?: z.ZodType<any>
7
+ handler: (options: any, context: { container: any }) => Promise<void>
8
+ }
9
+
10
+ const registry = new Map<string, CommandDefinition>()
11
+
12
+ export const commands = {
13
+ register(name: string, definition: CommandDefinition) {
14
+ registry.set(name, definition)
15
+ },
16
+
17
+ has(name: string): boolean {
18
+ return registry.has(name)
19
+ },
20
+
21
+ get(name: string): CommandDefinition | undefined {
22
+ return registry.get(name)
23
+ },
24
+
25
+ get available(): string[] {
26
+ return Array.from(registry.keys())
27
+ },
28
+ }