@digitalvibes/ai-knowledge-db 0.1.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/README.md +120 -0
- package/dist/cli.cjs +416 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +393 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +332 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +292 -0
- package/dist/index.js.map +1 -0
- package/migrations/001_init_knowledge.sql +30 -0
- package/package.json +63 -0
- package/skills/knowledge-db/SKILL.md +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @digitalvibes/ai-knowledge-db
|
|
2
|
+
|
|
3
|
+
AI knowledge **vector storage** for Dibe website/project work. A thin TypeScript
|
|
4
|
+
package over **Postgres + pgvector** (hosted on Hetzner via EasyPanel) that stores
|
|
5
|
+
**client-**, **project-**, and **global-scoped** knowledge and serves it back via
|
|
6
|
+
semantic search. Ships with a Claude **skill** and a CLI.
|
|
7
|
+
|
|
8
|
+
- ๐ **No secrets in the package.** Connection string, API key, and the default
|
|
9
|
+
client/project come from the *consuming* project's `.env`.
|
|
10
|
+
- ๐งญ **Scoped knowledge.** `global` โ `client` โ `project`. A project search
|
|
11
|
+
automatically also pulls that client's shared + global knowledge.
|
|
12
|
+
- ๐ค **Agent-ready.** A bundled skill (`skills/knowledge-db`) and a paste-in
|
|
13
|
+
Claude reference (`CLAUDE.reference.md`) teach agents to retrieve-before-build
|
|
14
|
+
and store durable decisions.
|
|
15
|
+
|
|
16
|
+
## Why pgvector + OpenAI
|
|
17
|
+
|
|
18
|
+
One Postgres instance holds both the relational metadata and the vectors โ
|
|
19
|
+
simplest thing to run and back up on EasyPanel. Embeddings use
|
|
20
|
+
`text-embedding-3-small` (1536 dims), cheap and good for brand/website knowledge.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @digitalvibes/ai-knowledge-db
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 1. Provision the DB (EasyPanel / Hetzner)
|
|
29
|
+
|
|
30
|
+
1. In EasyPanel, create a **Postgres** service (it includes a database).
|
|
31
|
+
2. Ensure the `vector` extension is available (the official `pgvector/pgvector`
|
|
32
|
+
image, or `CREATE EXTENSION vector;` on a Postgres with the extension installed).
|
|
33
|
+
3. Copy the connection string into your project's `.env` as `KNOWLEDGE_DB_URL`.
|
|
34
|
+
|
|
35
|
+
## 2. Configure the consuming project
|
|
36
|
+
|
|
37
|
+
Copy `.env.example` โ `.env` and fill in:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
KNOWLEDGE_DB_URL=postgres://user:pass@host:5432/knowledge
|
|
41
|
+
OPENAI_API_KEY=sk-...
|
|
42
|
+
KNOWLEDGE_CLIENT_ID=acme-corp
|
|
43
|
+
KNOWLEDGE_PROJECT_ID=acme-website-2026
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Initialise the schema once:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx knowledge-db init
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 3. Use it
|
|
53
|
+
|
|
54
|
+
### CLI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx knowledge-db add "Client prefers sentence case headings." --scope client
|
|
58
|
+
npx knowledge-db add-file ./brand/voice.md --scope client --source brand/voice.md
|
|
59
|
+
npx knowledge-db search "what is the brand voice?"
|
|
60
|
+
npx knowledge-db delete --source brand/voice.md
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Library
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createKnowledgeDB } from "@digitalvibes/ai-knowledge-db";
|
|
67
|
+
|
|
68
|
+
const kb = createKnowledgeDB(); // reads .env
|
|
69
|
+
|
|
70
|
+
await kb.add({ content: "Hero CTA finalised as 'Start free'.", source: "decisions.md" });
|
|
71
|
+
|
|
72
|
+
const hits = await kb.search("homepage hero copy", { limit: 5 });
|
|
73
|
+
console.log(hits.map((h) => `${h.score.toFixed(2)} ${h.content}`));
|
|
74
|
+
|
|
75
|
+
await kb.close();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
| Method | Purpose |
|
|
81
|
+
|--------|---------|
|
|
82
|
+
| `kb.init()` | Create extension, table, indexes (idempotent). |
|
|
83
|
+
| `kb.add(input)` | Chunk + embed + store. Scope/client/project fall back to env. |
|
|
84
|
+
| `kb.upsertSource(input)` | Idempotent re-ingest of a `source` (delete + re-add). |
|
|
85
|
+
| `kb.search(query, opts)` | Scoped semantic search โ ranked `SearchResult[]`. |
|
|
86
|
+
| `kb.delete(filter)` | Delete by id / source / project / client / scope. |
|
|
87
|
+
| `kb.close()` | Close the pool. |
|
|
88
|
+
|
|
89
|
+
### Scoping a search
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await kb.search("brand voice", {
|
|
93
|
+
clientId: "acme-corp",
|
|
94
|
+
projectId: "acme-website-2026",
|
|
95
|
+
includeClientKnowledge: true, // default โ also pull the client's shared rows
|
|
96
|
+
includeGlobal: true, // default โ also pull global rows
|
|
97
|
+
limit: 8,
|
|
98
|
+
minScore: 0.2,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## For agents
|
|
103
|
+
|
|
104
|
+
- **Skill:** `skills/knowledge-db/SKILL.md` โ full retrieve/add/update workflow.
|
|
105
|
+
- **Claude reference:** paste `CLAUDE.reference.md` into the consuming project's
|
|
106
|
+
`CLAUDE.md` so Claude knows the DB exists and uses it by default.
|
|
107
|
+
|
|
108
|
+
## Switching embedding model
|
|
109
|
+
|
|
110
|
+
`text-embedding-3-large` โ 3072 dims. Change `vector(1536)` โ `vector(3072)` in
|
|
111
|
+
`src/schema.sql`, re-run `init` on a fresh table, and set
|
|
112
|
+
`KNOWLEDGE_EMBED_MODEL=text-embedding-3-large`.
|
|
113
|
+
|
|
114
|
+
## Develop
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install
|
|
118
|
+
npm run build # tsup โ dist (esm + cjs + d.ts)
|
|
119
|
+
npm run typecheck
|
|
120
|
+
```
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_fs = require("fs");
|
|
28
|
+
|
|
29
|
+
// src/client.ts
|
|
30
|
+
var import_pg = __toESM(require("pg"), 1);
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var MODEL_DIMENSIONS = {
|
|
34
|
+
"text-embedding-3-small": 1536,
|
|
35
|
+
"text-embedding-3-large": 3072,
|
|
36
|
+
"text-embedding-ada-002": 1536
|
|
37
|
+
};
|
|
38
|
+
var env = (key) => {
|
|
39
|
+
const v = process.env[key];
|
|
40
|
+
return v && v.trim() !== "" ? v.trim() : void 0;
|
|
41
|
+
};
|
|
42
|
+
function resolveConfig(config = {}) {
|
|
43
|
+
const connectionString = config.connectionString ?? env("KNOWLEDGE_DB_URL");
|
|
44
|
+
if (!connectionString) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"[ai-knowledge-db] Missing connection string. Set KNOWLEDGE_DB_URL in your project's .env or pass { connectionString } to createKnowledgeDB()."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const openaiApiKey = config.openaiApiKey ?? env("OPENAI_API_KEY");
|
|
50
|
+
if (!openaiApiKey) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"[ai-knowledge-db] Missing OpenAI key. Set OPENAI_API_KEY in your project's .env or pass { openaiApiKey } to createKnowledgeDB()."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const embeddingModel = config.embeddingModel ?? env("KNOWLEDGE_EMBED_MODEL") ?? "text-embedding-3-small";
|
|
56
|
+
const embeddingDimensions = MODEL_DIMENSIONS[embeddingModel] ?? 1536;
|
|
57
|
+
return {
|
|
58
|
+
connectionString,
|
|
59
|
+
openaiApiKey,
|
|
60
|
+
embeddingModel,
|
|
61
|
+
embeddingDimensions,
|
|
62
|
+
clientId: config.clientId ?? env("KNOWLEDGE_CLIENT_ID"),
|
|
63
|
+
projectId: config.projectId ?? env("KNOWLEDGE_PROJECT_ID")
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/embeddings.ts
|
|
68
|
+
var import_openai = __toESM(require("openai"), 1);
|
|
69
|
+
var Embedder = class {
|
|
70
|
+
client;
|
|
71
|
+
model;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.client = new import_openai.default({ apiKey: config.openaiApiKey });
|
|
74
|
+
this.model = config.embeddingModel;
|
|
75
|
+
}
|
|
76
|
+
/** Embed a batch of strings in one API call. */
|
|
77
|
+
async embed(texts) {
|
|
78
|
+
if (texts.length === 0) return [];
|
|
79
|
+
const res = await this.client.embeddings.create({
|
|
80
|
+
model: this.model,
|
|
81
|
+
input: texts
|
|
82
|
+
});
|
|
83
|
+
return res.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
|
84
|
+
}
|
|
85
|
+
async embedOne(text) {
|
|
86
|
+
const [vec] = await this.embed([text]);
|
|
87
|
+
return vec;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function chunkText(text, opts = {}) {
|
|
91
|
+
const maxChars = opts.maxChars ?? 1200;
|
|
92
|
+
const overlap = opts.overlap ?? 150;
|
|
93
|
+
const clean = text.replace(/\r\n/g, "\n").trim();
|
|
94
|
+
if (clean.length <= maxChars) return clean ? [clean] : [];
|
|
95
|
+
const units = clean.split(/\n{2,}/).flatMap((p) => splitLongUnit(p, maxChars));
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let current = "";
|
|
98
|
+
for (const unit of units) {
|
|
99
|
+
if (current && current.length + unit.length + 2 > maxChars) {
|
|
100
|
+
chunks.push(current.trim());
|
|
101
|
+
current = overlap > 0 ? current.slice(-overlap) + "\n\n" + unit : unit;
|
|
102
|
+
} else {
|
|
103
|
+
current = current ? current + "\n\n" + unit : unit;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (current.trim()) chunks.push(current.trim());
|
|
107
|
+
return chunks;
|
|
108
|
+
}
|
|
109
|
+
function splitLongUnit(unit, maxChars) {
|
|
110
|
+
if (unit.length <= maxChars) return [unit];
|
|
111
|
+
const sentences = unit.match(/[^.!?\n]+[.!?]?\s*/g) ?? [unit];
|
|
112
|
+
const out = [];
|
|
113
|
+
let buf = "";
|
|
114
|
+
for (const s of sentences) {
|
|
115
|
+
if (s.length > maxChars) {
|
|
116
|
+
if (buf) {
|
|
117
|
+
out.push(buf);
|
|
118
|
+
buf = "";
|
|
119
|
+
}
|
|
120
|
+
for (let i = 0; i < s.length; i += maxChars) out.push(s.slice(i, i + maxChars));
|
|
121
|
+
} else if (buf.length + s.length > maxChars) {
|
|
122
|
+
out.push(buf);
|
|
123
|
+
buf = s;
|
|
124
|
+
} else {
|
|
125
|
+
buf += s;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (buf) out.push(buf);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/schema.ts
|
|
133
|
+
var SCHEMA_SQL = `-- AI Knowledge DB schema (Postgres + pgvector)
|
|
134
|
+
-- Run once against your Hetzner/EasyPanel Postgres instance, or via kb.init().
|
|
135
|
+
-- If you switch to text-embedding-3-large, change 1536 -> 3072 and re-index.
|
|
136
|
+
|
|
137
|
+
create extension if not exists vector;
|
|
138
|
+
create extension if not exists "pgcrypto"; -- for gen_random_uuid()
|
|
139
|
+
|
|
140
|
+
create table if not exists knowledge (
|
|
141
|
+
id uuid primary key default gen_random_uuid(),
|
|
142
|
+
scope text not null check (scope in ('global', 'client', 'project')),
|
|
143
|
+
client_id text,
|
|
144
|
+
project_id text,
|
|
145
|
+
source text,
|
|
146
|
+
content text not null,
|
|
147
|
+
embedding vector(1536) not null,
|
|
148
|
+
metadata jsonb not null default '{}',
|
|
149
|
+
created_at timestamptz not null default now()
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
create index if not exists knowledge_client_idx on knowledge (client_id);
|
|
153
|
+
create index if not exists knowledge_project_idx on knowledge (project_id);
|
|
154
|
+
create index if not exists knowledge_scope_idx on knowledge (scope);
|
|
155
|
+
create index if not exists knowledge_metadata_idx on knowledge using gin (metadata);
|
|
156
|
+
|
|
157
|
+
create index if not exists knowledge_embedding_idx
|
|
158
|
+
on knowledge using hnsw (embedding vector_cosine_ops);
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
// src/client.ts
|
|
162
|
+
var { Pool } = import_pg.default;
|
|
163
|
+
var KnowledgeDB = class {
|
|
164
|
+
pool;
|
|
165
|
+
embedder;
|
|
166
|
+
config;
|
|
167
|
+
constructor(config = {}) {
|
|
168
|
+
this.config = resolveConfig(config);
|
|
169
|
+
this.pool = new Pool({ connectionString: this.config.connectionString });
|
|
170
|
+
this.embedder = new Embedder(this.config);
|
|
171
|
+
}
|
|
172
|
+
/** Create the extension, table, and indexes if they don't exist. Safe to call repeatedly. */
|
|
173
|
+
async init() {
|
|
174
|
+
await this.pool.query(SCHEMA_SQL);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Add knowledge. The content is chunked, embedded, and stored. Returns the
|
|
178
|
+
* ids of the stored rows (one per chunk). Scope/client/project fall back to
|
|
179
|
+
* the env-configured defaults.
|
|
180
|
+
*/
|
|
181
|
+
async add(input) {
|
|
182
|
+
const clientId = input.clientId ?? this.config.clientId ?? null;
|
|
183
|
+
const projectId = input.projectId ?? this.config.projectId ?? null;
|
|
184
|
+
const scope = input.scope ?? defaultScope(clientId, projectId);
|
|
185
|
+
const source = input.source ?? null;
|
|
186
|
+
const metadata = input.metadata ?? {};
|
|
187
|
+
const chunks = chunkText(input.content, input.chunking);
|
|
188
|
+
if (chunks.length === 0) return [];
|
|
189
|
+
const vectors = await this.embedder.embed(chunks);
|
|
190
|
+
const ids = [];
|
|
191
|
+
const client = await this.pool.connect();
|
|
192
|
+
try {
|
|
193
|
+
await client.query("begin");
|
|
194
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
195
|
+
const chunkMeta = chunks.length > 1 ? { ...metadata, chunk: i, chunks: chunks.length } : metadata;
|
|
196
|
+
const { rows } = await client.query(
|
|
197
|
+
`insert into knowledge (scope, client_id, project_id, source, content, embedding, metadata)
|
|
198
|
+
values ($1, $2, $3, $4, $5, $6, $7) returning id`,
|
|
199
|
+
[scope, clientId, projectId, source, chunks[i], toVector(vectors[i]), chunkMeta]
|
|
200
|
+
);
|
|
201
|
+
ids.push(rows[0].id);
|
|
202
|
+
}
|
|
203
|
+
await client.query("commit");
|
|
204
|
+
} catch (err) {
|
|
205
|
+
await client.query("rollback");
|
|
206
|
+
throw err;
|
|
207
|
+
} finally {
|
|
208
|
+
client.release();
|
|
209
|
+
}
|
|
210
|
+
return ids;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Replace knowledge from a given source. Deletes existing rows that match the
|
|
214
|
+
* same scope + ids + source, then re-adds. Use this for idempotent re-ingest
|
|
215
|
+
* of a file or URL so you don't accumulate duplicates.
|
|
216
|
+
*/
|
|
217
|
+
async upsertSource(input) {
|
|
218
|
+
const clientId = input.clientId ?? this.config.clientId ?? null;
|
|
219
|
+
const projectId = input.projectId ?? this.config.projectId ?? null;
|
|
220
|
+
const scope = input.scope ?? defaultScope(clientId, projectId);
|
|
221
|
+
await this.delete({ scope, clientId: clientId ?? void 0, projectId: projectId ?? void 0, source: input.source });
|
|
222
|
+
return this.add(input);
|
|
223
|
+
}
|
|
224
|
+
/** Semantic search, scoped to client / project / global knowledge. */
|
|
225
|
+
async search(query, opts = {}) {
|
|
226
|
+
const clientId = opts.clientId ?? this.config.clientId;
|
|
227
|
+
const projectId = opts.projectId ?? this.config.projectId;
|
|
228
|
+
const includeClient = opts.includeClientKnowledge ?? true;
|
|
229
|
+
const includeGlobal = opts.includeGlobal ?? true;
|
|
230
|
+
const limit = opts.limit ?? 8;
|
|
231
|
+
const minScore = opts.minScore ?? 0;
|
|
232
|
+
const queryVec = toVector(await this.embedder.embedOne(query));
|
|
233
|
+
const orClauses = [];
|
|
234
|
+
const params = [queryVec];
|
|
235
|
+
const p = (v) => `$${params.push(v)}`;
|
|
236
|
+
if (projectId && allows(opts.scopes, "project")) {
|
|
237
|
+
orClauses.push(`(scope = 'project' and project_id = ${p(projectId)})`);
|
|
238
|
+
}
|
|
239
|
+
if (clientId && includeClient && allows(opts.scopes, "client")) {
|
|
240
|
+
orClauses.push(`(scope = 'client' and client_id = ${p(clientId)})`);
|
|
241
|
+
}
|
|
242
|
+
if (includeGlobal && allows(opts.scopes, "global")) {
|
|
243
|
+
orClauses.push(`scope = 'global'`);
|
|
244
|
+
}
|
|
245
|
+
const scopeClause = orClauses.length ? `(${orClauses.join(" or ")})` : `scope = 'global'`;
|
|
246
|
+
const where = [scopeClause];
|
|
247
|
+
if (opts.metadata) {
|
|
248
|
+
where.push(`metadata @> ${p(JSON.stringify(opts.metadata))}::jsonb`);
|
|
249
|
+
}
|
|
250
|
+
const { rows } = await this.pool.query(
|
|
251
|
+
`select id, scope, client_id, project_id, source, content, metadata, created_at,
|
|
252
|
+
1 - (embedding <=> $1) as score
|
|
253
|
+
from knowledge
|
|
254
|
+
where ${where.join(" and ")}
|
|
255
|
+
order by embedding <=> $1
|
|
256
|
+
limit ${p(limit)}`,
|
|
257
|
+
params
|
|
258
|
+
);
|
|
259
|
+
return rows.map(rowToResult).filter((r) => r.score >= minScore);
|
|
260
|
+
}
|
|
261
|
+
/** Delete rows matching a filter. Returns the number deleted. */
|
|
262
|
+
async delete(filter) {
|
|
263
|
+
const where = [];
|
|
264
|
+
const params = [];
|
|
265
|
+
const p = (v) => `$${params.push(v)}`;
|
|
266
|
+
if (filter.id) where.push(`id = ${p(filter.id)}`);
|
|
267
|
+
if (filter.scope) where.push(`scope = ${p(filter.scope)}`);
|
|
268
|
+
if (filter.clientId) where.push(`client_id = ${p(filter.clientId)}`);
|
|
269
|
+
if (filter.projectId) where.push(`project_id = ${p(filter.projectId)}`);
|
|
270
|
+
if (filter.source) where.push(`source = ${p(filter.source)}`);
|
|
271
|
+
if (where.length === 0) {
|
|
272
|
+
throw new Error("[ai-knowledge-db] delete() requires at least one filter to avoid wiping the table.");
|
|
273
|
+
}
|
|
274
|
+
const { rowCount } = await this.pool.query(
|
|
275
|
+
`delete from knowledge where ${where.join(" and ")}`,
|
|
276
|
+
params
|
|
277
|
+
);
|
|
278
|
+
return rowCount ?? 0;
|
|
279
|
+
}
|
|
280
|
+
/** Close the connection pool. Call on shutdown. */
|
|
281
|
+
async close() {
|
|
282
|
+
await this.pool.end();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
function createKnowledgeDB(config) {
|
|
286
|
+
return new KnowledgeDB(config);
|
|
287
|
+
}
|
|
288
|
+
function defaultScope(clientId, projectId) {
|
|
289
|
+
if (projectId) return "project";
|
|
290
|
+
if (clientId) return "client";
|
|
291
|
+
return "global";
|
|
292
|
+
}
|
|
293
|
+
function allows(scopes, scope) {
|
|
294
|
+
return !scopes || scopes.includes(scope);
|
|
295
|
+
}
|
|
296
|
+
function toVector(vec) {
|
|
297
|
+
return `[${vec.join(",")}]`;
|
|
298
|
+
}
|
|
299
|
+
function rowToResult(row) {
|
|
300
|
+
return { ...rowToRecord(row), score: Number(row.score) };
|
|
301
|
+
}
|
|
302
|
+
function rowToRecord(row) {
|
|
303
|
+
return {
|
|
304
|
+
id: row.id,
|
|
305
|
+
scope: row.scope,
|
|
306
|
+
clientId: row.client_id,
|
|
307
|
+
projectId: row.project_id,
|
|
308
|
+
source: row.source,
|
|
309
|
+
content: row.content,
|
|
310
|
+
metadata: row.metadata ?? {},
|
|
311
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/cli.ts
|
|
316
|
+
function parse(argv) {
|
|
317
|
+
const positional = [];
|
|
318
|
+
const flags = {};
|
|
319
|
+
for (let i = 0; i < argv.length; i++) {
|
|
320
|
+
const a = argv[i];
|
|
321
|
+
if (a === "--json") flags.json = true;
|
|
322
|
+
else if (a.startsWith("--")) flags[a.slice(2)] = argv[++i];
|
|
323
|
+
else positional.push(a);
|
|
324
|
+
}
|
|
325
|
+
return { positional, flags };
|
|
326
|
+
}
|
|
327
|
+
var USAGE = "Usage: knowledge-db <init|add|add-file|search|delete> [args] [flags]\nFlags: --scope --client --project --source --meta --limit --id --json";
|
|
328
|
+
async function main() {
|
|
329
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
330
|
+
if (!command || command === "help" || command === "-h" || command === "--help") {
|
|
331
|
+
console.log(USAGE);
|
|
332
|
+
process.exitCode = command ? 0 : 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const { positional, flags } = parse(rest);
|
|
336
|
+
const kb = createKnowledgeDB();
|
|
337
|
+
const meta = flags.meta ? JSON.parse(flags.meta) : void 0;
|
|
338
|
+
try {
|
|
339
|
+
switch (command) {
|
|
340
|
+
case "init": {
|
|
341
|
+
await kb.init();
|
|
342
|
+
console.log("\u2713 schema ready");
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "add": {
|
|
346
|
+
const ids = await kb.add({
|
|
347
|
+
content: positional.join(" "),
|
|
348
|
+
scope: flags.scope,
|
|
349
|
+
clientId: flags.client,
|
|
350
|
+
projectId: flags.project,
|
|
351
|
+
source: flags.source,
|
|
352
|
+
metadata: meta
|
|
353
|
+
});
|
|
354
|
+
console.log(`\u2713 stored ${ids.length} chunk(s)`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "add-file": {
|
|
358
|
+
const path = positional[0];
|
|
359
|
+
const content = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
360
|
+
const ids = await kb.upsertSource({
|
|
361
|
+
content,
|
|
362
|
+
source: flags.source ?? path,
|
|
363
|
+
scope: flags.scope,
|
|
364
|
+
clientId: flags.client,
|
|
365
|
+
projectId: flags.project,
|
|
366
|
+
metadata: meta
|
|
367
|
+
});
|
|
368
|
+
console.log(`\u2713 re-ingested ${path} \u2192 ${ids.length} chunk(s)`);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case "search": {
|
|
372
|
+
const results = await kb.search(positional.join(" "), {
|
|
373
|
+
clientId: flags.client,
|
|
374
|
+
projectId: flags.project,
|
|
375
|
+
limit: flags.limit ? Number(flags.limit) : void 0
|
|
376
|
+
});
|
|
377
|
+
if (flags.json) {
|
|
378
|
+
console.log(JSON.stringify(results, null, 2));
|
|
379
|
+
} else if (results.length === 0) {
|
|
380
|
+
console.log("(no matches)");
|
|
381
|
+
} else {
|
|
382
|
+
for (const r of results) {
|
|
383
|
+
console.log(
|
|
384
|
+
`
|
|
385
|
+
[${r.score.toFixed(3)}] ${r.scope}${r.source ? ` \xB7 ${r.source}` : ""}
|
|
386
|
+
${r.content}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "delete": {
|
|
393
|
+
const n = await kb.delete({
|
|
394
|
+
id: flags.id,
|
|
395
|
+
source: flags.source,
|
|
396
|
+
projectId: flags.project,
|
|
397
|
+
clientId: flags.client,
|
|
398
|
+
scope: flags.scope
|
|
399
|
+
});
|
|
400
|
+
console.log(`\u2713 deleted ${n} row(s)`);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
console.log(`Unknown command: ${command}
|
|
405
|
+
${USAGE}`);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
await kb.close();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
main().catch((err) => {
|
|
413
|
+
console.error(err instanceof Error ? err.message : err);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
});
|
|
416
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/client.ts","../src/config.ts","../src/embeddings.ts","../src/schema.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { createKnowledgeDB } from \"./client.js\";\nimport type { Scope } from \"./types.js\";\n\n/**\n * Thin CLI over the library so the skill (and humans) can manage knowledge\n * without writing code. Reads all connection/scope config from env\n * (KNOWLEDGE_DB_URL, OPENAI_API_KEY, KNOWLEDGE_CLIENT_ID, KNOWLEDGE_PROJECT_ID).\n *\n * knowledge-db init\n * knowledge-db add \"text...\" [--scope project] [--client X] [--project Y] [--source S] [--meta '{\"k\":\"v\"}']\n * knowledge-db add-file ./path.md [same flags] (re-ingest = idempotent upsert by source)\n * knowledge-db search \"query\" [--client X] [--project Y] [--limit 8] [--json]\n * knowledge-db delete [--id ID | --source S | --project Y | --client X | --scope S]\n */\n\ninterface Flags {\n scope?: Scope;\n client?: string;\n project?: string;\n source?: string;\n meta?: string;\n limit?: string;\n id?: string;\n json?: boolean;\n}\n\nfunction parse(argv: string[]): { positional: string[]; flags: Flags } {\n const positional: string[] = [];\n const flags: Flags = {};\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === \"--json\") flags.json = true;\n else if (a.startsWith(\"--\")) flags[a.slice(2) as keyof Flags] = argv[++i] as any;\n else positional.push(a);\n }\n return { positional, flags };\n}\n\nconst USAGE =\n \"Usage: knowledge-db <init|add|add-file|search|delete> [args] [flags]\\n\" +\n \"Flags: --scope --client --project --source --meta --limit --id --json\";\n\nasync function main() {\n const [command, ...rest] = process.argv.slice(2);\n\n if (!command || command === \"help\" || command === \"-h\" || command === \"--help\") {\n console.log(USAGE);\n process.exitCode = command ? 0 : 1;\n return;\n }\n\n const { positional, flags } = parse(rest);\n const kb = createKnowledgeDB();\n const meta = flags.meta ? JSON.parse(flags.meta) : undefined;\n\n try {\n switch (command) {\n case \"init\": {\n await kb.init();\n console.log(\"โ schema ready\");\n break;\n }\n case \"add\": {\n const ids = await kb.add({\n content: positional.join(\" \"),\n scope: flags.scope,\n clientId: flags.client,\n projectId: flags.project,\n source: flags.source,\n metadata: meta,\n });\n console.log(`โ stored ${ids.length} chunk(s)`);\n break;\n }\n case \"add-file\": {\n const path = positional[0];\n const content = readFileSync(path, \"utf8\");\n const ids = await kb.upsertSource({\n content,\n source: flags.source ?? path,\n scope: flags.scope,\n clientId: flags.client,\n projectId: flags.project,\n metadata: meta,\n });\n console.log(`โ re-ingested ${path} โ ${ids.length} chunk(s)`);\n break;\n }\n case \"search\": {\n const results = await kb.search(positional.join(\" \"), {\n clientId: flags.client,\n projectId: flags.project,\n limit: flags.limit ? Number(flags.limit) : undefined,\n });\n if (flags.json) {\n console.log(JSON.stringify(results, null, 2));\n } else if (results.length === 0) {\n console.log(\"(no matches)\");\n } else {\n for (const r of results) {\n console.log(\n `\\n[${r.score.toFixed(3)}] ${r.scope}${r.source ? ` ยท ${r.source}` : \"\"}\\n${r.content}`,\n );\n }\n }\n break;\n }\n case \"delete\": {\n const n = await kb.delete({\n id: flags.id,\n source: flags.source,\n projectId: flags.project,\n clientId: flags.client,\n scope: flags.scope,\n });\n console.log(`โ deleted ${n} row(s)`);\n break;\n }\n default:\n console.log(`Unknown command: ${command}\\n${USAGE}`);\n process.exitCode = 1;\n }\n } finally {\n await kb.close();\n }\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : err);\n process.exit(1);\n});\n","import pg from \"pg\";\nimport { resolveConfig, type KnowledgeConfig, type ResolvedConfig } from \"./config.js\";\nimport { Embedder, chunkText } from \"./embeddings.js\";\nimport { SCHEMA_SQL } from \"./schema.js\";\nimport type {\n AddInput,\n DeleteFilter,\n KnowledgeRecord,\n Scope,\n SearchOptions,\n SearchResult,\n} from \"./types.js\";\n\nconst { Pool } = pg;\n\nexport class KnowledgeDB {\n private pool: pg.Pool;\n private embedder: Embedder;\n readonly config: ResolvedConfig;\n\n constructor(config: KnowledgeConfig = {}) {\n this.config = resolveConfig(config);\n this.pool = new Pool({ connectionString: this.config.connectionString });\n this.embedder = new Embedder(this.config);\n }\n\n /** Create the extension, table, and indexes if they don't exist. Safe to call repeatedly. */\n async init(): Promise<void> {\n await this.pool.query(SCHEMA_SQL);\n }\n\n /**\n * Add knowledge. The content is chunked, embedded, and stored. Returns the\n * ids of the stored rows (one per chunk). Scope/client/project fall back to\n * the env-configured defaults.\n */\n async add(input: AddInput): Promise<string[]> {\n const clientId = input.clientId ?? this.config.clientId ?? null;\n const projectId = input.projectId ?? this.config.projectId ?? null;\n const scope = input.scope ?? defaultScope(clientId, projectId);\n const source = input.source ?? null;\n const metadata = input.metadata ?? {};\n\n const chunks = chunkText(input.content, input.chunking);\n if (chunks.length === 0) return [];\n\n const vectors = await this.embedder.embed(chunks);\n const ids: string[] = [];\n\n const client = await this.pool.connect();\n try {\n await client.query(\"begin\");\n for (let i = 0; i < chunks.length; i++) {\n const chunkMeta =\n chunks.length > 1\n ? { ...metadata, chunk: i, chunks: chunks.length }\n : metadata;\n const { rows } = await client.query(\n `insert into knowledge (scope, client_id, project_id, source, content, embedding, metadata)\n values ($1, $2, $3, $4, $5, $6, $7) returning id`,\n [scope, clientId, projectId, source, chunks[i], toVector(vectors[i]), chunkMeta],\n );\n ids.push(rows[0].id);\n }\n await client.query(\"commit\");\n } catch (err) {\n await client.query(\"rollback\");\n throw err;\n } finally {\n client.release();\n }\n return ids;\n }\n\n /**\n * Replace knowledge from a given source. Deletes existing rows that match the\n * same scope + ids + source, then re-adds. Use this for idempotent re-ingest\n * of a file or URL so you don't accumulate duplicates.\n */\n async upsertSource(input: AddInput & { source: string }): Promise<string[]> {\n const clientId = input.clientId ?? this.config.clientId ?? null;\n const projectId = input.projectId ?? this.config.projectId ?? null;\n const scope = input.scope ?? defaultScope(clientId, projectId);\n await this.delete({ scope, clientId: clientId ?? undefined, projectId: projectId ?? undefined, source: input.source });\n return this.add(input);\n }\n\n /** Semantic search, scoped to client / project / global knowledge. */\n async search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {\n const clientId = opts.clientId ?? this.config.clientId;\n const projectId = opts.projectId ?? this.config.projectId;\n const includeClient = opts.includeClientKnowledge ?? true;\n const includeGlobal = opts.includeGlobal ?? true;\n const limit = opts.limit ?? 8;\n const minScore = opts.minScore ?? 0;\n\n const queryVec = toVector(await this.embedder.embedOne(query));\n\n // Build a scope clause: project rows, optionally the client's shared rows,\n // optionally global rows โ restricted to the requested scopes if given.\n const orClauses: string[] = [];\n const params: unknown[] = [queryVec];\n const p = (v: unknown) => `$${params.push(v)}`;\n\n if (projectId && allows(opts.scopes, \"project\")) {\n orClauses.push(`(scope = 'project' and project_id = ${p(projectId)})`);\n }\n if (clientId && includeClient && allows(opts.scopes, \"client\")) {\n orClauses.push(`(scope = 'client' and client_id = ${p(clientId)})`);\n }\n if (includeGlobal && allows(opts.scopes, \"global\")) {\n orClauses.push(`scope = 'global'`);\n }\n // If nothing matched (e.g. no ids at all), fall back to global-only.\n const scopeClause = orClauses.length ? `(${orClauses.join(\" or \")})` : `scope = 'global'`;\n\n const where: string[] = [scopeClause];\n if (opts.metadata) {\n where.push(`metadata @> ${p(JSON.stringify(opts.metadata))}::jsonb`);\n }\n\n const { rows } = await this.pool.query(\n `select id, scope, client_id, project_id, source, content, metadata, created_at,\n 1 - (embedding <=> $1) as score\n from knowledge\n where ${where.join(\" and \")}\n order by embedding <=> $1\n limit ${p(limit)}`,\n params,\n );\n\n return rows\n .map(rowToResult)\n .filter((r) => r.score >= minScore);\n }\n\n /** Delete rows matching a filter. Returns the number deleted. */\n async delete(filter: DeleteFilter): Promise<number> {\n const where: string[] = [];\n const params: unknown[] = [];\n const p = (v: unknown) => `$${params.push(v)}`;\n if (filter.id) where.push(`id = ${p(filter.id)}`);\n if (filter.scope) where.push(`scope = ${p(filter.scope)}`);\n if (filter.clientId) where.push(`client_id = ${p(filter.clientId)}`);\n if (filter.projectId) where.push(`project_id = ${p(filter.projectId)}`);\n if (filter.source) where.push(`source = ${p(filter.source)}`);\n if (where.length === 0) {\n throw new Error(\"[ai-knowledge-db] delete() requires at least one filter to avoid wiping the table.\");\n }\n const { rowCount } = await this.pool.query(\n `delete from knowledge where ${where.join(\" and \")}`,\n params,\n );\n return rowCount ?? 0;\n }\n\n /** Close the connection pool. Call on shutdown. */\n async close(): Promise<void> {\n await this.pool.end();\n }\n}\n\nexport function createKnowledgeDB(config?: KnowledgeConfig): KnowledgeDB {\n return new KnowledgeDB(config);\n}\n\nfunction defaultScope(clientId: string | null, projectId: string | null): Scope {\n if (projectId) return \"project\";\n if (clientId) return \"client\";\n return \"global\";\n}\n\nfunction allows(scopes: Scope[] | undefined, scope: Scope): boolean {\n return !scopes || scopes.includes(scope);\n}\n\n/** pgvector accepts a vector literal like '[0.1,0.2,...]'. */\nfunction toVector(vec: number[]): string {\n return `[${vec.join(\",\")}]`;\n}\n\nfunction rowToResult(row: any): SearchResult {\n return { ...rowToRecord(row), score: Number(row.score) };\n}\n\nfunction rowToRecord(row: any): KnowledgeRecord {\n return {\n id: row.id,\n scope: row.scope,\n clientId: row.client_id,\n projectId: row.project_id,\n source: row.source,\n content: row.content,\n metadata: row.metadata ?? {},\n createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,\n };\n}\n","/**\n * All sensitive / per-deployment values live in the *consuming* project's\n * environment โ never in this package. A website repo that installs\n * `@dibe/ai-knowledge-db` sets these in its own `.env`:\n *\n * KNOWLEDGE_DB_URL=postgres://user:pass@host:5432/knowledge (Hetzner/EasyPanel)\n * OPENAI_API_KEY=sk-...\n * KNOWLEDGE_CLIENT_ID=acme-corp # default client for this repo\n * KNOWLEDGE_PROJECT_ID=acme-website-2026 # default project for this repo\n *\n * Anything passed explicitly to createKnowledgeDB() overrides the env value,\n * but env is the intended default so callers usually pass nothing.\n */\n\nexport interface KnowledgeConfig {\n /** Postgres connection string. Defaults to env KNOWLEDGE_DB_URL. */\n connectionString?: string;\n /** OpenAI API key. Defaults to env OPENAI_API_KEY. */\n openaiApiKey?: string;\n /** Embedding model. Defaults to env KNOWLEDGE_EMBED_MODEL or text-embedding-3-small. */\n embeddingModel?: string;\n /** Default client scope for this repo. Defaults to env KNOWLEDGE_CLIENT_ID. */\n clientId?: string;\n /** Default project scope for this repo. Defaults to env KNOWLEDGE_PROJECT_ID. */\n projectId?: string;\n}\n\nexport interface ResolvedConfig {\n connectionString: string;\n openaiApiKey: string;\n embeddingModel: string;\n embeddingDimensions: number;\n clientId?: string;\n projectId?: string;\n}\n\n/** text-embedding-3-small โ 1536, text-embedding-3-large โ 3072. */\nconst MODEL_DIMENSIONS: Record<string, number> = {\n \"text-embedding-3-small\": 1536,\n \"text-embedding-3-large\": 3072,\n \"text-embedding-ada-002\": 1536,\n};\n\nconst env = (key: string): string | undefined => {\n const v = process.env[key];\n return v && v.trim() !== \"\" ? v.trim() : undefined;\n};\n\nexport function resolveConfig(config: KnowledgeConfig = {}): ResolvedConfig {\n const connectionString = config.connectionString ?? env(\"KNOWLEDGE_DB_URL\");\n if (!connectionString) {\n throw new Error(\n \"[ai-knowledge-db] Missing connection string. Set KNOWLEDGE_DB_URL in your project's .env \" +\n \"or pass { connectionString } to createKnowledgeDB().\",\n );\n }\n\n const openaiApiKey = config.openaiApiKey ?? env(\"OPENAI_API_KEY\");\n if (!openaiApiKey) {\n throw new Error(\n \"[ai-knowledge-db] Missing OpenAI key. Set OPENAI_API_KEY in your project's .env \" +\n \"or pass { openaiApiKey } to createKnowledgeDB().\",\n );\n }\n\n const embeddingModel =\n config.embeddingModel ?? env(\"KNOWLEDGE_EMBED_MODEL\") ?? \"text-embedding-3-small\";\n const embeddingDimensions = MODEL_DIMENSIONS[embeddingModel] ?? 1536;\n\n return {\n connectionString,\n openaiApiKey,\n embeddingModel,\n embeddingDimensions,\n clientId: config.clientId ?? env(\"KNOWLEDGE_CLIENT_ID\"),\n projectId: config.projectId ?? env(\"KNOWLEDGE_PROJECT_ID\"),\n };\n}\n","import OpenAI from \"openai\";\nimport type { ResolvedConfig } from \"./config.js\";\nimport type { ChunkOptions } from \"./types.js\";\n\nexport class Embedder {\n private client: OpenAI;\n private model: string;\n\n constructor(config: ResolvedConfig) {\n this.client = new OpenAI({ apiKey: config.openaiApiKey });\n this.model = config.embeddingModel;\n }\n\n /** Embed a batch of strings in one API call. */\n async embed(texts: string[]): Promise<number[][]> {\n if (texts.length === 0) return [];\n const res = await this.client.embeddings.create({\n model: this.model,\n input: texts,\n });\n // OpenAI preserves input order in the response.\n return res.data\n .sort((a, b) => a.index - b.index)\n .map((d) => d.embedding as number[]);\n }\n\n async embedOne(text: string): Promise<number[]> {\n const [vec] = await this.embed([text]);\n return vec;\n }\n}\n\n/**\n * Split text into overlapping chunks. Prefers paragraph boundaries, then\n * sentence boundaries, falling back to hard character cuts for very long runs.\n */\nexport function chunkText(text: string, opts: ChunkOptions = {}): string[] {\n const maxChars = opts.maxChars ?? 1200;\n const overlap = opts.overlap ?? 150;\n const clean = text.replace(/\\r\\n/g, \"\\n\").trim();\n if (clean.length <= maxChars) return clean ? [clean] : [];\n\n // Split into paragraph-ish units first.\n const units = clean.split(/\\n{2,}/).flatMap((p) => splitLongUnit(p, maxChars));\n\n const chunks: string[] = [];\n let current = \"\";\n for (const unit of units) {\n if (current && current.length + unit.length + 2 > maxChars) {\n chunks.push(current.trim());\n // carry overlap from the tail of the previous chunk\n current = overlap > 0 ? current.slice(-overlap) + \"\\n\\n\" + unit : unit;\n } else {\n current = current ? current + \"\\n\\n\" + unit : unit;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n return chunks;\n}\n\n/** Break a single oversized paragraph on sentence, then hard, boundaries. */\nfunction splitLongUnit(unit: string, maxChars: number): string[] {\n if (unit.length <= maxChars) return [unit];\n const sentences = unit.match(/[^.!?\\n]+[.!?]?\\s*/g) ?? [unit];\n const out: string[] = [];\n let buf = \"\";\n for (const s of sentences) {\n if (s.length > maxChars) {\n if (buf) {\n out.push(buf);\n buf = \"\";\n }\n for (let i = 0; i < s.length; i += maxChars) out.push(s.slice(i, i + maxChars));\n } else if (buf.length + s.length > maxChars) {\n out.push(buf);\n buf = s;\n } else {\n buf += s;\n }\n }\n if (buf) out.push(buf);\n return out;\n}\n","/**\n * Canonical schema (Postgres + pgvector), kept as a string so the library never\n * has to read from disk โ works identically in the ESM and CJS builds. The\n * build also writes this out to dist/schema.sql for the `./schema.sql` export\n * and for running by hand. Vector size matches text-embedding-3-small (1536).\n */\nexport const SCHEMA_SQL = `-- AI Knowledge DB schema (Postgres + pgvector)\n-- Run once against your Hetzner/EasyPanel Postgres instance, or via kb.init().\n-- If you switch to text-embedding-3-large, change 1536 -> 3072 and re-index.\n\ncreate extension if not exists vector;\ncreate extension if not exists \"pgcrypto\"; -- for gen_random_uuid()\n\ncreate table if not exists knowledge (\n id uuid primary key default gen_random_uuid(),\n scope text not null check (scope in ('global', 'client', 'project')),\n client_id text,\n project_id text,\n source text,\n content text not null,\n embedding vector(1536) not null,\n metadata jsonb not null default '{}',\n created_at timestamptz not null default now()\n);\n\ncreate index if not exists knowledge_client_idx on knowledge (client_id);\ncreate index if not exists knowledge_project_idx on knowledge (project_id);\ncreate index if not exists knowledge_scope_idx on knowledge (scope);\ncreate index if not exists knowledge_metadata_idx on knowledge using gin (metadata);\n\ncreate index if not exists knowledge_embedding_idx\n on knowledge using hnsw (embedding vector_cosine_ops);\n`;\n"],"mappings":"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","names":["OpenAI","pg"]}
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|