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.
- package/CNAME +1 -0
- package/LAUNCH-PLAN.md +419 -0
- package/README.md +90 -0
- package/index.html +1636 -0
- package/package.json +5 -4
- package/src/adapters/node-fs.ts +80 -0
- package/src/cli/commands/api/endpoints/doc.ts +2 -2
- package/src/cli/commands/api/endpoints/document.ts +2 -2
- package/src/cli/commands/api/endpoints/search-reindex.ts +6 -25
- package/src/cli/commands/api/endpoints/search-status.ts +3 -20
- package/src/cli/commands/api/endpoints/search.ts +3 -33
- package/src/cli/commands/api/endpoints/semantic-search.ts +3 -33
- package/src/cli/commands/embed.ts +8 -81
- package/src/cli/commands/mcp.ts +5 -1128
- package/src/cli/commands/search.ts +25 -90
- package/src/cli/commands/serve.ts +7 -32
- package/src/cli/index.ts +1 -1
- package/src/cli/load-collection.ts +2 -2
- package/src/collection.ts +42 -33
- package/src/document.ts +18 -0
- package/src/extract-sections.ts +3 -0
- package/src/index.ts +10 -0
- package/src/mcp/helpers.ts +7 -0
- package/src/mcp/model-info.ts +102 -0
- package/src/mcp/prompts.ts +199 -0
- package/src/mcp/readme.ts +153 -0
- package/src/mcp/resources.ts +89 -0
- package/src/mcp/server.ts +103 -0
- package/src/mcp/tools/mutation.ts +176 -0
- package/src/mcp/tools/query.ts +141 -0
- package/src/mcp/tools/search.ts +180 -0
- package/src/parse.ts +5 -0
- package/src/query/query-dsl.ts +1 -1
- package/src/relationships/has-many.ts +4 -2
- package/src/search/document-inputs.ts +85 -0
- package/src/search/luca-semantic-search.ts +75 -0
- package/src/types.ts +54 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/match-pattern.ts +2 -2
- package/src/utils/strip-markdown.ts +59 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contentbase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"repository": "https://github.com/soederpop/contentbase",
|
|
5
5
|
"website": "https://contentbase.soederpop.com",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "./
|
|
8
|
-
"types": "./
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
10
|
"cnotes": "./src/cli/index.ts",
|
|
11
11
|
"contentbase": "./src/cli/index.ts"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "vitest run",
|
|
15
15
|
"test:watch": "vitest",
|
|
16
|
+
"build": "find dist -name '*.d.ts' -o -name '*.d.ts.map' -o -name '*.js' -o -name '*.js.map' | xargs rm -f 2>/dev/null; tsc",
|
|
16
17
|
"typecheck": "tsc --noEmit",
|
|
17
18
|
"examples": "bun run scripts/examples/run-all.ts",
|
|
18
19
|
"compile": "bun build ./src/cli/index.ts --compile --outfile dist/cnotes --external @node-llama-cpp/*",
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
"release": "./scripts/release.sh"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"
|
|
24
|
+
"luca": ">= 3.0.2",
|
|
24
25
|
"gray-matter": "^4.0.3",
|
|
25
26
|
"js-yaml": "^4.1.0",
|
|
26
27
|
"mdast-util-mdxjs-esm": "^2.0.1",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { StorageAdapter, FileEntry, FileStat } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default StorageAdapter that reads and writes from the local file system
|
|
7
|
+
* using Node's fs/promises module. This is used automatically when no
|
|
8
|
+
* adapter is passed to the Collection constructor.
|
|
9
|
+
*/
|
|
10
|
+
export class NodeStorageAdapter implements StorageAdapter {
|
|
11
|
+
async listFiles(rootPath: string, match: RegExp): Promise<FileEntry[]> {
|
|
12
|
+
const results: FileEntry[] = [];
|
|
13
|
+
|
|
14
|
+
const scan = async (dirPath: string) => {
|
|
15
|
+
let entries: string[];
|
|
16
|
+
try {
|
|
17
|
+
entries = await fs.readdir(dirPath);
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.startsWith(".")) continue;
|
|
24
|
+
|
|
25
|
+
const filePath = path.join(dirPath, entry);
|
|
26
|
+
const stat = await fs.stat(filePath);
|
|
27
|
+
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
await scan(filePath);
|
|
30
|
+
} else if (stat.isFile() && match.test(filePath)) {
|
|
31
|
+
results.push({
|
|
32
|
+
key: filePath,
|
|
33
|
+
stat: {
|
|
34
|
+
createdAt: stat.ctime,
|
|
35
|
+
updatedAt: stat.mtime,
|
|
36
|
+
size: stat.size,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
await scan(rootPath);
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async readFile(key: string): Promise<string> {
|
|
48
|
+
return fs.readFile(key, "utf8");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async stat(key: string): Promise<FileStat> {
|
|
52
|
+
const s = await fs.stat(key);
|
|
53
|
+
return { createdAt: s.ctime, updatedAt: s.mtime, size: s.size };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async writeFile(key: string, content: string): Promise<void> {
|
|
57
|
+
await fs.mkdir(path.parse(key).dir, { recursive: true });
|
|
58
|
+
await fs.writeFile(key, content, "utf8");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async deleteFile(key: string): Promise<void> {
|
|
62
|
+
try {
|
|
63
|
+
await fs.rm(key);
|
|
64
|
+
} catch {
|
|
65
|
+
// Silently ignore missing files — matches existing behavior
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
join(root: string, ...parts: string[]): string {
|
|
70
|
+
return path.resolve(root, ...parts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
relative(root: string, key: string): string {
|
|
74
|
+
return path.relative(root, key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
extname(key: string): string {
|
|
78
|
+
return path.extname(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -173,8 +173,8 @@ ${html}
|
|
|
173
173
|
if ('fetchAll' in rel) {
|
|
174
174
|
(result.relationships as any)[key] = rel.fetchAll().map((i: any) => ({ id: i.id, title: i.title }))
|
|
175
175
|
} else if ('fetch' in rel) {
|
|
176
|
-
const
|
|
177
|
-
(result.relationships as any)[key] =
|
|
176
|
+
const parentInstance = rel.fetch();
|
|
177
|
+
(result.relationships as any)[key] = parentInstance ? { id: parentInstance.id, title: parentInstance.title } : null
|
|
178
178
|
}
|
|
179
179
|
} catch {}
|
|
180
180
|
}
|
|
@@ -58,8 +58,8 @@ export async function get(params: any, ctx: any) {
|
|
|
58
58
|
if ('fetchAll' in rel) {
|
|
59
59
|
(result.relationships as any)[key] = rel.fetchAll().map((i: any) => ({ id: i.id, title: i.title }))
|
|
60
60
|
} else if ('fetch' in rel) {
|
|
61
|
-
const
|
|
62
|
-
(result.relationships as any)[key] =
|
|
61
|
+
const parentInstance = rel.fetch();
|
|
62
|
+
(result.relationships as any)[key] = parentInstance ? { id: parentInstance.id, title: parentInstance.title } : null
|
|
63
63
|
}
|
|
64
64
|
} catch {}
|
|
65
65
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import
|
|
2
|
+
import { collectDocumentInputs } from '../../../../search/document-inputs.js'
|
|
3
|
+
import { getInitializedSemanticSearch } from '../../../../search/luca-semantic-search.js'
|
|
3
4
|
|
|
4
5
|
export const path = '/api/search/reindex'
|
|
5
6
|
export const description = 'Trigger search index rebuild'
|
|
@@ -14,14 +15,7 @@ export async function post(params: any, ctx: any) {
|
|
|
14
15
|
const collection = ctx.container._contentbaseCollection
|
|
15
16
|
const rootPath = collection.rootPath
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
if (!ctx.container.features.available.includes('semanticSearch')) {
|
|
19
|
-
SemanticSearch.attach(ctx.container)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
|
|
23
|
-
const ss = ctx.container.feature('semanticSearch', { dbPath })
|
|
24
|
-
await ss.initDb()
|
|
18
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
25
19
|
|
|
26
20
|
if (params.pathIds) {
|
|
27
21
|
await ss.reindex(params.pathIds)
|
|
@@ -30,23 +24,10 @@ export async function post(params: any, ctx: any) {
|
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
// Collect and re-index documents
|
|
33
|
-
const docs: any[] = []
|
|
34
27
|
const targetIds = params.pathIds || collection.available
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const doc = collection.document(pathId)
|
|
39
|
-
const modelDef = collection.findModelDefinition(pathId)
|
|
40
|
-
|
|
41
|
-
docs.push({
|
|
42
|
-
pathId,
|
|
43
|
-
model: modelDef?.name ?? undefined,
|
|
44
|
-
title: doc.title,
|
|
45
|
-
slug: doc.slug,
|
|
46
|
-
meta: doc.meta,
|
|
47
|
-
content: doc.content,
|
|
48
|
-
})
|
|
49
|
-
}
|
|
28
|
+
const targetSet = new Set(targetIds)
|
|
29
|
+
const docs = collectDocumentInputs(collection)
|
|
30
|
+
.filter((doc) => targetSet.has(doc.pathId))
|
|
50
31
|
|
|
51
32
|
const toIndex = params.force ? docs : docs.filter((d: any) => ss.needsReindex(d))
|
|
52
33
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import
|
|
3
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { getInitializedSemanticSearch, hasSearchIndex } from '../../../../search/luca-semantic-search.js'
|
|
4
3
|
|
|
5
4
|
export const path = '/api/search/status'
|
|
6
5
|
export const description = 'Search index health and statistics'
|
|
@@ -11,16 +10,7 @@ export const getSchema = z.object({})
|
|
|
11
10
|
export async function get(_params: any, ctx: any) {
|
|
12
11
|
const collection = ctx.container._contentbaseCollection
|
|
13
12
|
const rootPath = collection.rootPath
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const hasIndex = existsSync(dbDir) && (() => {
|
|
17
|
-
try {
|
|
18
|
-
const files = readdirSync(dbDir) as string[]
|
|
19
|
-
return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
|
|
20
|
-
} catch {
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
})()
|
|
13
|
+
const hasIndex = hasSearchIndex(rootPath)
|
|
24
14
|
|
|
25
15
|
if (!hasIndex) {
|
|
26
16
|
return {
|
|
@@ -37,14 +27,7 @@ export async function get(_params: any, ctx: any) {
|
|
|
37
27
|
}
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
const
|
|
41
|
-
if (!ctx.container.features.available.includes('semanticSearch')) {
|
|
42
|
-
SemanticSearch.attach(ctx.container)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
|
|
46
|
-
const ss = ctx.container.feature('semanticSearch', { dbPath })
|
|
47
|
-
await ss.initDb()
|
|
30
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
48
31
|
|
|
49
32
|
const stats = ss.getStats()
|
|
50
33
|
return {
|
|
@@ -1,40 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import
|
|
3
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { getInitializedSemanticSearch, hasSearchIndex } from '../../../../search/luca-semantic-search.js'
|
|
4
3
|
|
|
5
4
|
export const path = '/api/search'
|
|
6
5
|
export const description = 'Search across collection documents using keyword, semantic, or hybrid modes'
|
|
7
6
|
export const tags = ['query']
|
|
8
7
|
|
|
9
|
-
// ── Helpers ──────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
function hasSearchIndex(rootPath: string): boolean {
|
|
12
|
-
const dbDir = pathModule.join(rootPath, '.contentbase')
|
|
13
|
-
if (!existsSync(dbDir)) return false
|
|
14
|
-
try {
|
|
15
|
-
const files = readdirSync(dbDir) as string[]
|
|
16
|
-
return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
|
|
17
|
-
} catch {
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let _semanticSearch: any = null
|
|
23
|
-
|
|
24
|
-
async function getSemanticSearch(container: any, rootPath: string) {
|
|
25
|
-
if (_semanticSearch?.state?.get('dbReady')) return _semanticSearch
|
|
26
|
-
|
|
27
|
-
const { SemanticSearch } = await import('@soederpop/luca/agi')
|
|
28
|
-
if (!container.features.available.includes('semanticSearch')) {
|
|
29
|
-
SemanticSearch.attach(container)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
|
|
33
|
-
_semanticSearch = container.feature('semanticSearch', { dbPath })
|
|
34
|
-
await _semanticSearch.initDb()
|
|
35
|
-
return _semanticSearch
|
|
36
|
-
}
|
|
37
|
-
|
|
38
8
|
async function doSearch(ss: any, query: string, mode: string, options: any) {
|
|
39
9
|
switch (mode) {
|
|
40
10
|
case 'keyword':
|
|
@@ -65,7 +35,7 @@ export async function get(params: any, ctx: any) {
|
|
|
65
35
|
return { error: 'No search index found. Run: cnotes embed' }
|
|
66
36
|
}
|
|
67
37
|
|
|
68
|
-
const ss = await
|
|
38
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
69
39
|
const mode = params.mode || 'hybrid'
|
|
70
40
|
const limit = params.limit ? parseInt(params.limit, 10) : 10
|
|
71
41
|
const searchOptions = { limit, model: params.model }
|
|
@@ -92,7 +62,7 @@ export async function post(params: any, ctx: any) {
|
|
|
92
62
|
return { error: 'No search index found. Run: cnotes embed' }
|
|
93
63
|
}
|
|
94
64
|
|
|
95
|
-
const ss = await
|
|
65
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
96
66
|
const mode = params.mode || 'hybrid'
|
|
97
67
|
const searchOptions = {
|
|
98
68
|
limit: params.limit || 10,
|
|
@@ -1,40 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import
|
|
3
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { getInitializedSemanticSearch, hasSearchIndex } from '../../../../search/luca-semantic-search.js'
|
|
4
3
|
|
|
5
4
|
export const path = '/api/semantic-search'
|
|
6
5
|
export const description = 'Semantic search across collection documents'
|
|
7
6
|
export const tags = ['query']
|
|
8
7
|
|
|
9
|
-
// ── Helpers ──────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
function hasSearchIndex(rootPath: string): boolean {
|
|
12
|
-
const dbDir = pathModule.join(rootPath, '.contentbase')
|
|
13
|
-
if (!existsSync(dbDir)) return false
|
|
14
|
-
try {
|
|
15
|
-
const files = readdirSync(dbDir) as string[]
|
|
16
|
-
return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
|
|
17
|
-
} catch {
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let _semanticSearch: any = null
|
|
23
|
-
|
|
24
|
-
async function getSemanticSearch(container: any, rootPath: string) {
|
|
25
|
-
if (_semanticSearch?.state?.get('dbReady')) return _semanticSearch
|
|
26
|
-
|
|
27
|
-
const { SemanticSearch } = await import('@soederpop/luca/agi')
|
|
28
|
-
if (!container.features.available.includes('semanticSearch')) {
|
|
29
|
-
SemanticSearch.attach(container)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
|
|
33
|
-
_semanticSearch = container.feature('semanticSearch', { dbPath })
|
|
34
|
-
await _semanticSearch.initDb()
|
|
35
|
-
return _semanticSearch
|
|
36
|
-
}
|
|
37
|
-
|
|
38
8
|
// ── GET /api/semantic-search ─────────────────────────────────────────
|
|
39
9
|
|
|
40
10
|
export const getSchema = z.object({
|
|
@@ -53,7 +23,7 @@ export async function get(params: any, ctx: any) {
|
|
|
53
23
|
return { error: 'No search index found. Run: cnotes embed' }
|
|
54
24
|
}
|
|
55
25
|
|
|
56
|
-
const ss = await
|
|
26
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
57
27
|
const mode = params.mode || 'hybrid'
|
|
58
28
|
const limit = params.limit ? parseInt(params.limit, 10) : 10
|
|
59
29
|
const searchOptions = { limit, model: params.model }
|
|
@@ -94,7 +64,7 @@ export async function post(params: any, ctx: any) {
|
|
|
94
64
|
return { error: 'No search index found. Run: cnotes embed' }
|
|
95
65
|
}
|
|
96
66
|
|
|
97
|
-
const ss = await
|
|
67
|
+
const ss = await getInitializedSemanticSearch(ctx.container, rootPath)
|
|
98
68
|
const mode = params.mode || 'hybrid'
|
|
99
69
|
const searchOptions = {
|
|
100
70
|
limit: params.limit || 10,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, accessSync, constants as fsConstants } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { commands } from '../registry.js'
|
|
5
5
|
import { loadCollection } from '../load-collection.js'
|
|
6
|
+
import { collectDocumentInputs } from '../../search/document-inputs.js'
|
|
7
|
+
import { createSemanticSearch, ensureSemanticSearchAttached, getInitializedSemanticSearch, hasSearchIndex, loadSemanticSearchClass } from '../../search/luca-semantic-search.js'
|
|
6
8
|
|
|
7
9
|
const argsSchema = z.object({
|
|
8
10
|
force: z.boolean().default(false),
|
|
@@ -13,67 +15,6 @@ const argsSchema = z.object({
|
|
|
13
15
|
contentFolder: z.string().optional(),
|
|
14
16
|
})
|
|
15
17
|
|
|
16
|
-
function hasSearchIndex(rootPath: string): boolean {
|
|
17
|
-
const dbDir = path.join(rootPath, '.contentbase')
|
|
18
|
-
if (!existsSync(dbDir)) return false
|
|
19
|
-
try {
|
|
20
|
-
const files = readdirSync(dbDir) as string[]
|
|
21
|
-
return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
|
|
22
|
-
} catch {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function collectDocumentInputs(collection: any) {
|
|
28
|
-
const inputs: any[] = []
|
|
29
|
-
for (const pathId of collection.available) {
|
|
30
|
-
const doc = collection.document(pathId)
|
|
31
|
-
const modelDef = collection.findModelDefinition(pathId)
|
|
32
|
-
|
|
33
|
-
const sections: any[] = []
|
|
34
|
-
const lines = (doc.content as string).split('\n')
|
|
35
|
-
let currentHeading: string | null = null
|
|
36
|
-
let currentContent: string[] = []
|
|
37
|
-
|
|
38
|
-
for (const line of lines) {
|
|
39
|
-
const h2Match = line.match(/^## (.+)/)
|
|
40
|
-
if (h2Match) {
|
|
41
|
-
if (currentHeading) {
|
|
42
|
-
sections.push({
|
|
43
|
-
heading: currentHeading,
|
|
44
|
-
headingPath: currentHeading,
|
|
45
|
-
content: currentContent.join('\n').trim(),
|
|
46
|
-
level: 2,
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
currentHeading = h2Match[1].trim()
|
|
50
|
-
currentContent = []
|
|
51
|
-
} else if (currentHeading) {
|
|
52
|
-
currentContent.push(line)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (currentHeading) {
|
|
56
|
-
sections.push({
|
|
57
|
-
heading: currentHeading,
|
|
58
|
-
headingPath: currentHeading,
|
|
59
|
-
content: currentContent.join('\n').trim(),
|
|
60
|
-
level: 2,
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
inputs.push({
|
|
65
|
-
pathId,
|
|
66
|
-
model: modelDef?.name ?? undefined,
|
|
67
|
-
title: doc.title,
|
|
68
|
-
slug: doc.slug,
|
|
69
|
-
meta: doc.meta,
|
|
70
|
-
content: doc.content,
|
|
71
|
-
sections: sections.length > 0 ? sections : undefined,
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
return inputs
|
|
75
|
-
}
|
|
76
|
-
|
|
77
18
|
function isLocalInstalled(): boolean {
|
|
78
19
|
const modulePath = path.join(process.cwd(), 'node_modules', 'node-llama-cpp')
|
|
79
20
|
try {
|
|
@@ -103,10 +44,8 @@ async function installLocal(SemanticSearch: any, container: any): Promise<void>
|
|
|
103
44
|
console.error(`Installing node-llama-cpp@${SemanticSearch.PINNED_LLAMA_VERSION}...`)
|
|
104
45
|
|
|
105
46
|
// Create a temporary instance to use the install method
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
const ss = container.feature('semanticSearch', {
|
|
47
|
+
await ensureSemanticSearchAttached(container, SemanticSearch)
|
|
48
|
+
const ss = await createSemanticSearch(container, cwd, {
|
|
110
49
|
dbPath: path.join(cwd, '.contentbase/search.sqlite'),
|
|
111
50
|
embeddingProvider: 'local',
|
|
112
51
|
})
|
|
@@ -138,7 +77,7 @@ function detectInstallCommand(cwd: string, version: string): string {
|
|
|
138
77
|
}
|
|
139
78
|
|
|
140
79
|
async function handler(options: z.infer<typeof argsSchema>, { container }: { container: any }) {
|
|
141
|
-
const
|
|
80
|
+
const SemanticSearch = await loadSemanticSearchClass()
|
|
142
81
|
|
|
143
82
|
// --install-local: install node-llama-cpp only, then exit
|
|
144
83
|
if (options.installLocal) {
|
|
@@ -171,15 +110,9 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
|
|
|
171
110
|
return
|
|
172
111
|
}
|
|
173
112
|
|
|
174
|
-
|
|
175
|
-
SemanticSearch.attach(container)
|
|
176
|
-
}
|
|
177
|
-
const dbPath = path.join(collection.rootPath, '.contentbase/search.sqlite')
|
|
178
|
-
const ss = container.feature('semanticSearch', {
|
|
179
|
-
dbPath,
|
|
113
|
+
const ss = await getInitializedSemanticSearch(container, collection.rootPath, {
|
|
180
114
|
embeddingProvider: provider,
|
|
181
115
|
})
|
|
182
|
-
await ss.initDb()
|
|
183
116
|
|
|
184
117
|
const stats = ss.getStats()
|
|
185
118
|
console.log('Search Index Status')
|
|
@@ -206,15 +139,9 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
|
|
|
206
139
|
}
|
|
207
140
|
|
|
208
141
|
// Embed / re-embed
|
|
209
|
-
|
|
210
|
-
SemanticSearch.attach(container)
|
|
211
|
-
}
|
|
212
|
-
const dbPath = path.join(collection.rootPath, '.contentbase/search.sqlite')
|
|
213
|
-
const ss = container.feature('semanticSearch', {
|
|
214
|
-
dbPath,
|
|
142
|
+
const ss = await getInitializedSemanticSearch(container, collection.rootPath, {
|
|
215
143
|
embeddingProvider: provider,
|
|
216
144
|
})
|
|
217
|
-
await ss.initDb()
|
|
218
145
|
|
|
219
146
|
const docs = collectDocumentInputs(collection)
|
|
220
147
|
const startTime = Date.now()
|