@nzpr/kb 0.1.4 → 0.1.6
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 +11 -1
- package/lib/admin-cli.js +2 -0
- package/lib/cli.js +2 -0
- package/lib/dotenv.js +67 -0
- package/lib/init-repo-interactive.js +23 -102
- package/lib/repo-init.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,14 @@ npm install -g @nzpr/kb
|
|
|
66
66
|
kb --help
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
Environment-driven setup:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cp .env.example .env
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The CLI automatically reads `.env` from the current repository, so `kb init-repo`, `kb create`, `kb publish`, `kb search`, and `kb ask` can all use the values defined there.
|
|
76
|
+
|
|
69
77
|
From source:
|
|
70
78
|
|
|
71
79
|
```bash
|
|
@@ -103,6 +111,8 @@ If you want the CLI to walk you through setup and tell you what to do next:
|
|
|
103
111
|
kb init-repo --interactive
|
|
104
112
|
```
|
|
105
113
|
|
|
114
|
+
The interactive flow checks the environment it can see, reports what is possible, and tells you which values are still missing from `.env`.
|
|
115
|
+
|
|
106
116
|
Layout options:
|
|
107
117
|
|
|
108
118
|
- `--layout repo-root` puts documents in `docs/`
|
|
@@ -141,9 +151,9 @@ Inside that knowledge repo, the normal flow is:
|
|
|
141
151
|
In interactive mode it also:
|
|
142
152
|
|
|
143
153
|
- asks which repo and directory you want to initialize
|
|
154
|
+
- checks the environment values already available
|
|
144
155
|
- asks whether to wire GitHub setup now
|
|
145
156
|
- asks whether to verify the database now
|
|
146
|
-
- collects embedding settings if you want remote embeddings
|
|
147
157
|
- prints the next actions after initialization
|
|
148
158
|
|
|
149
159
|
If one remote step fails, the scaffold still stays in place and the command tells you what to rerun.
|
package/lib/admin-cli.js
CHANGED
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
tryResolveKnowledgeRoot
|
|
8
8
|
} from "./config.js";
|
|
9
9
|
import { connect, initDb, schemaStatus } from "./db.js";
|
|
10
|
+
import { loadDotEnv } from "./dotenv.js";
|
|
10
11
|
import { ingestDocuments } from "./index.js";
|
|
11
12
|
import { writeProposalDocument } from "./kb-proposals.js";
|
|
12
13
|
import { databaseHelp, formatCliError, maskConnection, parseFlags } from "./cli-common.js";
|
|
13
14
|
|
|
14
15
|
export async function main(argv) {
|
|
15
16
|
try {
|
|
17
|
+
loadDotEnv();
|
|
16
18
|
const [command, ...rest] = argv;
|
|
17
19
|
if (!command || command === "--help" || command === "-h") {
|
|
18
20
|
printHelp();
|
package/lib/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
tryResolveKnowledgeRoot
|
|
9
9
|
} from "./config.js";
|
|
10
10
|
import { connect, initDb } from "./db.js";
|
|
11
|
+
import { loadDotEnv } from "./dotenv.js";
|
|
11
12
|
import { ingestDocuments } from "./index.js";
|
|
12
13
|
import { collectInitRepoInteractiveOptions } from "./init-repo-interactive.js";
|
|
13
14
|
import { createGitHubIssueFromText } from "./kb-proposals.js";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
|
|
25
26
|
export async function main(argv) {
|
|
26
27
|
try {
|
|
28
|
+
loadDotEnv();
|
|
27
29
|
const [command, ...rest] = argv;
|
|
28
30
|
if (!command || command === "--help" || command === "-h") {
|
|
29
31
|
printHelp();
|
package/lib/dotenv.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function loadDotEnv({ cwd = process.cwd(), env = process.env } = {}) {
|
|
5
|
+
const envPath = findDotEnv(cwd);
|
|
6
|
+
if (!envPath) {
|
|
7
|
+
return { path: null, loaded: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const parsed = parseDotEnv(fs.readFileSync(envPath, "utf8"));
|
|
11
|
+
const loaded = [];
|
|
12
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
13
|
+
if (env[key] === undefined || env[key] === "") {
|
|
14
|
+
env[key] = value;
|
|
15
|
+
loaded.push(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { path: envPath, loaded };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseDotEnv(content) {
|
|
23
|
+
const values = {};
|
|
24
|
+
for (const rawLine of String(content ?? "").split(/\r?\n/)) {
|
|
25
|
+
const line = rawLine.trim();
|
|
26
|
+
if (!line || line.startsWith("#")) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
30
|
+
if (!match) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const [, key, rawValue] = match;
|
|
34
|
+
values[key] = normalizeEnvValue(rawValue);
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findDotEnv(start) {
|
|
40
|
+
let current = path.resolve(start);
|
|
41
|
+
while (true) {
|
|
42
|
+
const candidate = path.join(current, ".env");
|
|
43
|
+
if (fs.existsSync(candidate)) {
|
|
44
|
+
return candidate;
|
|
45
|
+
}
|
|
46
|
+
const parent = path.dirname(current);
|
|
47
|
+
if (parent === current) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
current = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeEnvValue(rawValue) {
|
|
55
|
+
const trimmed = rawValue.trim();
|
|
56
|
+
if (
|
|
57
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
58
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
59
|
+
) {
|
|
60
|
+
return trimmed.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
const commentIndex = trimmed.indexOf(" #");
|
|
63
|
+
if (commentIndex >= 0) {
|
|
64
|
+
return trimmed.slice(0, commentIndex).trim();
|
|
65
|
+
}
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
3
|
import readline from "node:readline/promises";
|
|
5
4
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
6
5
|
import { resolveGitHubRepository } from "./config.js";
|
|
@@ -11,8 +10,7 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
11
10
|
cwd = process.cwd(),
|
|
12
11
|
stdin = defaultStdin,
|
|
13
12
|
stdout = defaultStdout,
|
|
14
|
-
prompter = null
|
|
15
|
-
auth = defaultInteractiveGitHubAuth
|
|
13
|
+
prompter = null
|
|
16
14
|
}) {
|
|
17
15
|
if (!prompter && (!stdin.isTTY || !stdout.isTTY)) {
|
|
18
16
|
throw new Error("interactive init-repo requires a TTY; rerun in a terminal or pass flags directly");
|
|
@@ -33,6 +31,7 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
33
31
|
})
|
|
34
32
|
);
|
|
35
33
|
const layout = await askKnowledgeLayout(prompt, targetDir, defaults.layout);
|
|
34
|
+
printEnvCapabilities(stdout, defaults);
|
|
36
35
|
|
|
37
36
|
const configureRepo = await prompt.askConfirm(
|
|
38
37
|
"configure the GitHub repo now",
|
|
@@ -45,13 +44,8 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
45
44
|
validate: requireValue
|
|
46
45
|
});
|
|
47
46
|
if (!defaults.githubToken) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
defaults.githubToken = authorized;
|
|
51
|
-
} else {
|
|
52
|
-
stdout.write("skipping GitHub repo setup for now because GitHub auth is not ready.\n");
|
|
53
|
-
repo = null;
|
|
54
|
-
}
|
|
47
|
+
stdout.write("skipping GitHub repo setup for now because GITHUB_TOKEN is not set. Put it in .env and rerun.\n");
|
|
48
|
+
repo = null;
|
|
55
49
|
}
|
|
56
50
|
}
|
|
57
51
|
|
|
@@ -60,62 +54,21 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
60
54
|
Boolean(defaults.databaseUrl)
|
|
61
55
|
);
|
|
62
56
|
|
|
63
|
-
let databaseUrl = null;
|
|
64
|
-
if (verifyDatabase) {
|
|
65
|
-
|
|
66
|
-
validate: requireValue
|
|
67
|
-
});
|
|
57
|
+
let databaseUrl = verifyDatabase ? defaults.databaseUrl : null;
|
|
58
|
+
if (verifyDatabase && !databaseUrl) {
|
|
59
|
+
stdout.write("skipping database preflight for now because KB_DATABASE_URL is not set. Put it in .env and rerun.\n");
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let embeddingModel = null;
|
|
73
|
-
let embeddingApiKey = null;
|
|
74
|
-
let dbConnectTimeoutMs = null;
|
|
75
|
-
|
|
76
|
-
if (verifyDatabase) {
|
|
77
|
-
const useRemoteEmbeddings = await prompt.askConfirm(
|
|
78
|
-
"configure remote embeddings now",
|
|
79
|
-
defaults.embeddingMode === "bge-m3-openai"
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
if (useRemoteEmbeddings) {
|
|
83
|
-
embeddingMode = "bge-m3-openai";
|
|
84
|
-
embeddingApiUrl = await prompt.askText(
|
|
85
|
-
"embedding API URL",
|
|
86
|
-
defaults.embeddingApiUrl || "https://your-embeddings-host/v1/embeddings",
|
|
87
|
-
{ validate: requireValue }
|
|
88
|
-
);
|
|
89
|
-
embeddingModel = await prompt.askText(
|
|
90
|
-
"embedding model",
|
|
91
|
-
defaults.embeddingModel || "BAAI/bge-m3",
|
|
92
|
-
{ validate: requireValue }
|
|
93
|
-
);
|
|
94
|
-
embeddingApiKey = await prompt.askText(
|
|
95
|
-
"embedding API key (optional)",
|
|
96
|
-
defaults.embeddingApiKey
|
|
97
|
-
);
|
|
98
|
-
dbConnectTimeoutMs = await prompt.askText(
|
|
99
|
-
"database connect timeout ms",
|
|
100
|
-
defaults.dbConnectTimeoutMs || "20000",
|
|
101
|
-
{ validate: requirePositiveNumber }
|
|
102
|
-
);
|
|
103
|
-
}
|
|
62
|
+
if (!databaseUrl) {
|
|
63
|
+
databaseUrl = null;
|
|
104
64
|
}
|
|
105
65
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (useAutomationToken) {
|
|
113
|
-
repoAutomationToken = await prompt.askText(
|
|
114
|
-
"repo automation token",
|
|
115
|
-
defaults.repoAutomationToken
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
66
|
+
const embeddingMode = defaults.embeddingMode || null;
|
|
67
|
+
const embeddingApiUrl = defaults.embeddingApiUrl || null;
|
|
68
|
+
const embeddingModel = defaults.embeddingModel || null;
|
|
69
|
+
const embeddingApiKey = defaults.embeddingApiKey || null;
|
|
70
|
+
const dbConnectTimeoutMs = defaults.dbConnectTimeoutMs || null;
|
|
71
|
+
const repoAutomationToken = defaults.repoAutomationToken || null;
|
|
119
72
|
|
|
120
73
|
stdout.write("\nPlan\n");
|
|
121
74
|
stdout.write(` target directory: ${targetDir}\n`);
|
|
@@ -265,44 +218,12 @@ function emptyToNull(value) {
|
|
|
265
218
|
return normalized ? normalized : null;
|
|
266
219
|
}
|
|
267
220
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
stdout.write("The wizard can open GitHub CLI login in your browser and then continue.\n");
|
|
277
|
-
const loginNow = await prompt.askConfirm("authorize GitHub CLI now", true);
|
|
278
|
-
if (!loginNow) {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
auth.login();
|
|
283
|
-
const token = auth.getToken();
|
|
284
|
-
if (token) {
|
|
285
|
-
stdout.write("GitHub CLI authentication is ready.\n");
|
|
286
|
-
return token;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
throw new Error("GitHub authentication did not complete; rerun gh auth login or set GITHUB_TOKEN");
|
|
221
|
+
function printEnvCapabilities(stdout, defaults) {
|
|
222
|
+
stdout.write("Environment check\n");
|
|
223
|
+
stdout.write(` .env GitHub token: ${defaults.githubToken ? "found" : "missing"}\n`);
|
|
224
|
+
stdout.write(` .env target repo: ${defaults.repo ? defaults.repo : "missing"}\n`);
|
|
225
|
+
stdout.write(` .env database URL: ${defaults.databaseUrl ? "found" : "missing"}\n`);
|
|
226
|
+
stdout.write(
|
|
227
|
+
` .env embeddings: ${defaults.embeddingMode ? defaults.embeddingMode : "not configured"}\n\n`
|
|
228
|
+
);
|
|
290
229
|
}
|
|
291
|
-
|
|
292
|
-
const defaultInteractiveGitHubAuth = {
|
|
293
|
-
getToken() {
|
|
294
|
-
try {
|
|
295
|
-
return execFileSync("gh", ["auth", "token"], {
|
|
296
|
-
encoding: "utf8",
|
|
297
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
298
|
-
}).trim();
|
|
299
|
-
} catch {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
login() {
|
|
304
|
-
execFileSync("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
|
|
305
|
-
stdio: "inherit"
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
};
|
package/lib/repo-init.js
CHANGED
|
@@ -28,6 +28,7 @@ export function initializeKnowledgeRepo({
|
|
|
28
28
|
const normalizedLayout = normalizeKnowledgeLayout(layout);
|
|
29
29
|
const docsRootRelative = docsRootForLayout(normalizedLayout);
|
|
30
30
|
const files = new Map([
|
|
31
|
+
[".env.example", renderDotEnvExample()],
|
|
31
32
|
[".github/ISSUE_TEMPLATE/config.yml", renderIssueConfig()],
|
|
32
33
|
[".github/ISSUE_TEMPLATE/knowledge-document.md", renderIssueTemplate({ docsRootRelative })],
|
|
33
34
|
[".github/workflows/kb-issue-to-pr.yml", renderIssueToPrWorkflow({ docsRootRelative })],
|
|
@@ -360,6 +361,21 @@ function renderIssueConfig() {
|
|
|
360
361
|
return ["blank_issues_enabled: false", ""].join("\n");
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
function renderDotEnvExample() {
|
|
365
|
+
return [
|
|
366
|
+
"# Copy this file to .env and fill in the values you want to use locally.",
|
|
367
|
+
"KB_GITHUB_REPO=owner/repo",
|
|
368
|
+
"GITHUB_TOKEN=",
|
|
369
|
+
"KB_DATABASE_URL=postgresql://kb:kb@localhost:5432/kb",
|
|
370
|
+
"KB_EMBEDDING_MODE=local-hash",
|
|
371
|
+
"# KB_EMBEDDING_API_URL=https://your-embeddings-host/v1/embeddings",
|
|
372
|
+
"# KB_EMBEDDING_MODEL=BAAI/bge-m3",
|
|
373
|
+
"# KB_EMBEDDING_API_KEY=",
|
|
374
|
+
"# KB_DB_CONNECT_TIMEOUT_MS=20000",
|
|
375
|
+
"# KB_REPO_AUTOMATION_TOKEN="
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
363
379
|
function renderIssueTemplate({ docsRootRelative }) {
|
|
364
380
|
return [
|
|
365
381
|
"---",
|