@nzpr/kb 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/.env.example ADDED
@@ -0,0 +1,5 @@
1
+ KB_DATABASE_URL=postgresql://kb:kb@localhost:5432/kb
2
+ KB_EMBEDDING_MODE=local-hash
3
+ # KB_EMBEDDING_API_URL=http://127.0.0.1:8000/v1/embeddings
4
+ # KB_EMBEDDING_MODEL=BAAI/bge-m3
5
+ # KB_EMBEDDING_API_KEY=
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nzpr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # Team Knowledge Base CLI
2
+
3
+ `@nzpr/kb` is the npm package and CLI used by a separate knowledge-authority repository.
4
+
5
+ Each KB instance is defined by:
6
+
7
+ - `KB_DATABASE_URL`
8
+ - `KB_GITHUB_REPO`
9
+
10
+ This repository is for the tool itself.
11
+
12
+ The actual knowledge base should live in a separate repository that owns:
13
+
14
+ - `kb/docs/`
15
+ - issue templates
16
+ - review and approval workflow
17
+ - CI that runs `kb publish`
18
+
19
+ Bootstrap that repo with:
20
+
21
+ ```bash
22
+ kb init-repo --dir /path/to/knowledge-repo
23
+ ```
24
+
25
+ Or bootstrap and configure it in one step:
26
+
27
+ ```bash
28
+ export GITHUB_TOKEN=...
29
+ kb init-repo \
30
+ --dir /path/to/knowledge-repo \
31
+ --repo owner/knowledge-repo \
32
+ --database-url postgresql://kb:kb@host:5432/kb \
33
+ --embedding-mode bge-m3-openai \
34
+ --embedding-api-url https://embeddings.example.com/v1/embeddings \
35
+ --embedding-model BAAI/bge-m3
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ - `kb init-repo [--dir PATH] [--repo OWNER/REPO]`
41
+ - `kb search "<terms>"`
42
+ - `kb ask "<question>"`
43
+ - `kb list`
44
+ - `kb catalog [--json]`
45
+ - `kb create --title TEXT --text TEXT [--path RELATIVE_PATH]`
46
+ - `kb publish --docs-root PATH`
47
+
48
+ ## Install
49
+
50
+ From npm:
51
+
52
+ ```bash
53
+ npm install -g @nzpr/kb
54
+ kb --help
55
+ ```
56
+
57
+ From source:
58
+
59
+ ```bash
60
+ npm install
61
+ npm link
62
+ ```
63
+
64
+ ## Knowledge Repo Workflow
65
+
66
+ In the separate knowledge-authority repo:
67
+
68
+ 1. Run `kb init-repo` once to scaffold the repo. If you provide `--repo`, `--database-url`, and `GITHUB_TOKEN`, it also configures the target repo and preflights the database.
69
+ 2. Open or update a knowledge proposal issue.
70
+ 3. Review and approve it there.
71
+ 4. Materialize it into `kb/docs/`.
72
+ 5. After merge, that repo's CI runs `kb publish --docs-root ./kb/docs`.
73
+
74
+ `kb init-repo` is safe to rerun. It reports bootstrap status for:
75
+
76
+ - local scaffold creation
77
+ - database preflight and schema initialization
78
+ - GitHub repo labels, variables, and secrets
79
+
80
+ If one remote step fails, the scaffold still stays in place and the command tells you what to rerun.
81
+
82
+ ## Runtime
83
+
84
+ Node.js 20+ is required.
85
+
86
+ Query commands need:
87
+
88
+ ```bash
89
+ export KB_DATABASE_URL=postgresql://kb:kb@localhost:5432/kb
90
+ ```
91
+
92
+ Higher-quality embeddings with a self-hosted OpenAI-compatible `BAAI/bge-m3` server:
93
+
94
+ ```bash
95
+ export KB_EMBEDDING_MODE=bge-m3-openai
96
+ export KB_EMBEDDING_API_URL=https://embeddings.example.com/v1/embeddings
97
+ export KB_EMBEDDING_MODEL=BAAI/bge-m3
98
+ export KB_EMBEDDING_API_KEY=...
99
+ ```
100
+
101
+ If `KB_EMBEDDING_MODE` is omitted, the CLI uses local hash embeddings for development.
102
+
103
+ Publishing needs one more privileged environment variable:
104
+
105
+ ```bash
106
+ export KB_GITHUB_REPO=owner/repo
107
+ export GITHUB_TOKEN=...
108
+ ```
109
+
110
+ Publishing is allowed when the provided `GITHUB_TOKEN` has write access to `KB_GITHUB_REPO`. Normal readers do not need GitHub credentials.
111
+
112
+ When `kb init-repo` is given `--repo`, it uses the same token to:
113
+
114
+ - enable issues in the target repo
115
+ - create or update the `kb-entry` and `kb-approved` labels
116
+ - write repository secrets and variables
117
+ - verify and initialize the target database schema if `--database-url` is provided
118
+
119
+ If you run `kb init-repo` without the repo or database inputs, it still scaffolds the knowledge repo and prints exactly which remote bootstrap inputs are still pending.
120
+
121
+ ## Quick Start
122
+
123
+ ```bash
124
+ export KB_DATABASE_URL=postgresql://kb:kb@localhost:5432/kb
125
+ export KB_GITHUB_REPO=owner/repo
126
+ export GITHUB_TOKEN=...
127
+ docker compose -f docker-compose.pgvector.yml up -d
128
+ kb publish --docs-root ./kb/docs
129
+ kb catalog --json
130
+ kb search "deployment rule"
131
+ ```
132
+
133
+ Each Markdown file is indexed as one document and one vector row. Keep documents simple: one title and one body.
134
+
135
+ ## Optional Split Mode
136
+
137
+ If you later want separate entities, isolated databases, or a dedicated content repo, you can use `kb create` with:
138
+
139
+ ```bash
140
+ export KB_GITHUB_REPO=org/knowledge-content
141
+ export GITHUB_TOKEN=...
142
+ ```
143
+
144
+ That path is optional. The default assumption is that you work in one repo.
145
+
146
+ ## npm Release
147
+
148
+ The package is published as `@nzpr/kb`.
149
+
150
+ - local validation: `npm test`
151
+ - local package preview: `npm pack --dry-run`
152
+ - GitHub Actions publish: push a tag like `v0.1.0` or run the `npm-publish` workflow manually
153
+ - required repository secret: `NPM_TOKEN`
154
+
155
+ ## Using From A Knowledge Repo
156
+
157
+ The knowledge-authority repo should install this package in CI and use it to sync `kb/docs/` into the vector database:
158
+
159
+ ```bash
160
+ npm install -g @nzpr/kb
161
+ export KB_GITHUB_REPO=owner/repo
162
+ export GITHUB_TOKEN=...
163
+ kb publish --docs-root ./kb/docs
164
+ ```
165
+
166
+ Or scaffold that repo first:
167
+
168
+ ```bash
169
+ kb init-repo --dir .
170
+ ```
171
+
172
+ If you want init to configure the target repo too:
173
+
174
+ ```bash
175
+ export GITHUB_TOKEN=...
176
+ export KB_REPO_AUTOMATION_TOKEN=...
177
+ kb init-repo \
178
+ --dir . \
179
+ --repo owner/knowledge-repo \
180
+ --database-url postgresql://kb:kb@host:5432/kb \
181
+ --embedding-mode bge-m3-openai \
182
+ --embedding-api-url https://embeddings.example.com/v1/embeddings \
183
+ --embedding-model BAAI/bge-m3 \
184
+ --embedding-api-key YOUR_KEY
185
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../lib/admin-cli.js";
4
+
5
+ process.exitCode = await main(process.argv.slice(2));
package/bin/kb.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../lib/cli.js";
4
+
5
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,19 @@
1
+ services:
2
+ postgres:
3
+ image: pgvector/pgvector:pg16
4
+ environment:
5
+ POSTGRES_DB: kb
6
+ POSTGRES_USER: kb
7
+ POSTGRES_PASSWORD: kb
8
+ ports:
9
+ - "5432:5432"
10
+ volumes:
11
+ - kb_pgdata:/var/lib/postgresql/data
12
+ healthcheck:
13
+ test: ["CMD-SHELL", "pg_isready -U kb -d kb"]
14
+ interval: 10s
15
+ timeout: 5s
16
+ retries: 10
17
+
18
+ volumes:
19
+ kb_pgdata:
@@ -0,0 +1,203 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ resolveDatabaseUrl,
5
+ resolveDocsRoot,
6
+ resolveEmbeddingProfile,
7
+ tryResolveKnowledgeRoot
8
+ } from "./config.js";
9
+ import { connect, initDb, schemaStatus } from "./db.js";
10
+ import { ingestDocuments } from "./index.js";
11
+ import { writeProposalDocument } from "./kb-proposals.js";
12
+ import { databaseHelp, formatCliError, maskConnection, parseFlags } from "./cli-common.js";
13
+
14
+ export async function main(argv) {
15
+ try {
16
+ const [command, ...rest] = argv;
17
+ if (!command || command === "--help" || command === "-h") {
18
+ printHelp();
19
+ return 0;
20
+ }
21
+ if (rest.includes("--help") || rest.includes("-h")) {
22
+ printCommandHelp(command);
23
+ return 0;
24
+ }
25
+
26
+ const { flags } = parseFlags(rest);
27
+ switch (command) {
28
+ case "migrate": {
29
+ const databaseUrl = requireDatabaseUrl();
30
+ const client = await connect(databaseUrl);
31
+ try {
32
+ const result = await initDb(client);
33
+ console.log(
34
+ `migrated knowledge database: ${maskConnection(databaseUrl)} current=${result.currentVersion} applied=${result.appliedCount}`
35
+ );
36
+ } finally {
37
+ await client.end();
38
+ }
39
+ return 0;
40
+ }
41
+ case "init-db": {
42
+ const databaseUrl = requireDatabaseUrl();
43
+ const client = await connect(databaseUrl);
44
+ try {
45
+ const result = await initDb(client);
46
+ console.log(
47
+ `initialized knowledge database: ${maskConnection(databaseUrl)} current=${result.currentVersion} applied=${result.appliedCount}`
48
+ );
49
+ } finally {
50
+ await client.end();
51
+ }
52
+ return 0;
53
+ }
54
+ case "status": {
55
+ const databaseUrl = requireDatabaseUrl();
56
+ const client = await connect(databaseUrl);
57
+ try {
58
+ const status = await schemaStatus(client);
59
+ console.log(`database: ${maskConnection(databaseUrl)}`);
60
+ console.log(`schema current: ${status.currentVersion}`);
61
+ console.log(`schema latest: ${status.latestVersion}`);
62
+ console.log(`schema pending: ${status.pendingCount}`);
63
+ } finally {
64
+ await client.end();
65
+ }
66
+ return 0;
67
+ }
68
+ case "ingest": {
69
+ const databaseUrl = requireDatabaseUrl();
70
+ const embeddingProfile = resolveEmbeddingProfile();
71
+ const knowledgeRoot = flags["knowledge-root"]
72
+ ? path.resolve(flags["knowledge-root"])
73
+ : tryResolveKnowledgeRoot();
74
+ const docsRoot = resolveDocsRoot({
75
+ docsRoot: flags["docs-root"] ?? null,
76
+ knowledgeRoot
77
+ });
78
+ if (!docsRoot) {
79
+ console.error("--docs-root PATH is required when no local docs directory is present");
80
+ return 2;
81
+ }
82
+ if (!fs.existsSync(docsRoot)) {
83
+ console.error(`docs root does not exist: ${docsRoot}`);
84
+ return 2;
85
+ }
86
+ const result = await ingestDocuments({ databaseUrl, docsRoot, embeddingProfile });
87
+ console.log(
88
+ `indexed ${result.documents} documents, wrote ${result.vectors} vectors using ${result.embeddingMode}${result.embeddingModel ? `/${result.embeddingModel}` : ""} embeddings`
89
+ );
90
+ return 0;
91
+ }
92
+ case "issue-to-doc": {
93
+ const knowledgeRoot = flags["knowledge-root"]
94
+ ? path.resolve(flags["knowledge-root"])
95
+ : tryResolveKnowledgeRoot();
96
+ const docsRoot = resolveDocsRoot({
97
+ docsRoot: flags["docs-root"] ?? null,
98
+ knowledgeRoot
99
+ });
100
+ if (!docsRoot) {
101
+ console.error("--docs-root PATH is required when no local docs directory is present");
102
+ return 2;
103
+ }
104
+ const issueEventPath = flags["issue-event"] ? path.resolve(flags["issue-event"]) : null;
105
+ if (!issueEventPath) {
106
+ console.error("--issue-event PATH is required");
107
+ return 2;
108
+ }
109
+ if (!fs.existsSync(issueEventPath)) {
110
+ console.error(`issue event payload does not exist: ${issueEventPath}`);
111
+ return 2;
112
+ }
113
+ const event = JSON.parse(fs.readFileSync(issueEventPath, "utf8"));
114
+ const issue = event.issue;
115
+ if (!issue?.body) {
116
+ throw new Error("issue event payload did not include an issue body");
117
+ }
118
+ const result = writeProposalDocument({
119
+ issueNumber: issue.number,
120
+ issueTitle: issue.title,
121
+ issueBody: issue.body,
122
+ docsRoot
123
+ });
124
+ writeGitHubOutput({
125
+ doc_id: result.proposal.docId,
126
+ doc_path: path.relative(process.cwd(), result.absolutePath).replace(/\\/g, "/"),
127
+ branch: result.branch,
128
+ commit_message: result.commitMessage,
129
+ pr_title: result.prTitle,
130
+ pr_body: result.prBody
131
+ });
132
+ console.log(`materialized issue #${issue.number} into ${result.relativePath}`);
133
+ return 0;
134
+ }
135
+ default:
136
+ console.error(`unknown command: ${command}`);
137
+ return 2;
138
+ }
139
+ } catch (error) {
140
+ console.error(formatCliError(error));
141
+ return 1;
142
+ }
143
+ }
144
+
145
+ function printHelp() {
146
+ console.log(`usage: kb-admin <command> [options]
147
+
148
+ internal automation only; normal users should use kb
149
+
150
+ commands:
151
+ migrate
152
+ init-db
153
+ status
154
+ ingest
155
+ issue-to-doc
156
+
157
+ ${databaseHelp()}
158
+
159
+ ingest options:
160
+ --docs-root PATH
161
+
162
+ issue-to-doc options:
163
+ --issue-event PATH
164
+ --docs-root PATH
165
+
166
+ optional path hint:
167
+ --knowledge-root PATH
168
+ `);
169
+ }
170
+
171
+ function printCommandHelp(command) {
172
+ const commandHelp = {
173
+ migrate: `usage: kb-admin migrate\n\n${databaseHelp()}`,
174
+ "init-db": `usage: kb-admin init-db\n\n${databaseHelp()}\n\ninit-db is a bootstrap alias for migrate.`,
175
+ status: `usage: kb-admin status\n\n${databaseHelp()}`,
176
+ ingest: `usage: kb-admin ingest [--docs-root PATH] [--knowledge-root PATH]\n\n${databaseHelp()}`,
177
+ "issue-to-doc": `usage: kb-admin issue-to-doc --issue-event PATH [--docs-root PATH]\n\nInternal automation for approved KB issues.`
178
+ };
179
+ console.log(commandHelp[command] ?? `unknown command: ${command}`);
180
+ }
181
+
182
+ function requireDatabaseUrl() {
183
+ const databaseUrl = resolveDatabaseUrl();
184
+ if (!databaseUrl) {
185
+ throw new Error("KB_DATABASE_URL is not set");
186
+ }
187
+ return databaseUrl;
188
+ }
189
+
190
+ function writeGitHubOutput(entries) {
191
+ const outputPath = process.env.GITHUB_OUTPUT ?? null;
192
+ if (!outputPath) {
193
+ return;
194
+ }
195
+ for (const [key, value] of Object.entries(entries)) {
196
+ const normalized = String(value);
197
+ if (normalized.includes("\n")) {
198
+ fs.appendFileSync(outputPath, `${key}<<__KB_EOF__\n${normalized}\n__KB_EOF__\n`, "utf8");
199
+ continue;
200
+ }
201
+ fs.appendFileSync(outputPath, `${key}=${normalized}\n`, "utf8");
202
+ }
203
+ }
@@ -0,0 +1,16 @@
1
+ export function chunkMarkdown(meta, body) {
2
+ const content = String(body ?? "").trim();
3
+ if (!content) {
4
+ return [];
5
+ }
6
+ return [
7
+ {
8
+ chunkId: `${meta.docId}#0`,
9
+ docId: meta.docId,
10
+ title: meta.title,
11
+ heading: meta.title,
12
+ content,
13
+ path: meta.path,
14
+ }
15
+ ];
16
+ }
@@ -0,0 +1,73 @@
1
+ export function parseFlags(args) {
2
+ const flags = {};
3
+ const positional = [];
4
+ for (let index = 0; index < args.length; index += 1) {
5
+ const arg = args[index];
6
+ if (!arg.startsWith("--")) {
7
+ positional.push(arg);
8
+ continue;
9
+ }
10
+ const key = arg.slice(2);
11
+ if (key === "json" || key === "include-drafts") {
12
+ flags[key] = true;
13
+ continue;
14
+ }
15
+ const value = args[index + 1];
16
+ if (value === undefined || value.startsWith("--")) {
17
+ throw new Error(`missing value for --${key}`);
18
+ }
19
+ flags[key] = value;
20
+ index += 1;
21
+ }
22
+ return { flags, positional };
23
+ }
24
+
25
+ export function maskConnection(connectionString) {
26
+ try {
27
+ const url = new URL(connectionString);
28
+ if (url.password) {
29
+ url.password = "****";
30
+ }
31
+ return url.toString();
32
+ } catch {
33
+ return connectionString;
34
+ }
35
+ }
36
+
37
+ export function formatCliError(error) {
38
+ const message = String(error?.message ?? error);
39
+ if (
40
+ /ECONNREFUSED|connect ECONNREFUSED|ENOTFOUND|timeout expired|Connection terminated unexpectedly/i.test(
41
+ message
42
+ )
43
+ ) {
44
+ return `database connection failed: ${message}`;
45
+ }
46
+ return message;
47
+ }
48
+
49
+ export function databaseHelp() {
50
+ return `required runtime:
51
+ KB_DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DB
52
+
53
+ optional embeddings:
54
+ KB_EMBEDDING_MODE=local-hash|bge-m3-openai
55
+ KB_EMBEDDING_API_URL=http://HOST:8000/v1/embeddings
56
+ KB_EMBEDDING_MODEL=BAAI/bge-m3
57
+ KB_EMBEDDING_API_KEY=...`;
58
+ }
59
+
60
+ export function githubCreationHelp() {
61
+ return `required GitHub issue creation runtime:
62
+ KB_GITHUB_REPO=OWNER/REPO
63
+ GITHUB_TOKEN=...
64
+
65
+ optional GitHub API override:
66
+ GITHUB_API_URL=https://api.github.com`;
67
+ }
68
+
69
+ export function publishHelp() {
70
+ return `required publish authorization:
71
+ KB_GITHUB_REPO=OWNER/REPO
72
+ GITHUB_TOKEN=...`;
73
+ }