@nzpr/kb 0.1.6 → 0.1.8
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 +1 -11
- package/lib/admin-cli.js +0 -2
- package/lib/cli.js +0 -2
- package/lib/init-repo-interactive.js +183 -23
- package/lib/repo-init.js +0 -16
- package/package.json +1 -1
- package/lib/dotenv.js +0 -67
package/README.md
CHANGED
|
@@ -66,14 +66,6 @@ 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
|
-
|
|
77
69
|
From source:
|
|
78
70
|
|
|
79
71
|
```bash
|
|
@@ -111,8 +103,6 @@ If you want the CLI to walk you through setup and tell you what to do next:
|
|
|
111
103
|
kb init-repo --interactive
|
|
112
104
|
```
|
|
113
105
|
|
|
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
|
-
|
|
116
106
|
Layout options:
|
|
117
107
|
|
|
118
108
|
- `--layout repo-root` puts documents in `docs/`
|
|
@@ -151,9 +141,9 @@ Inside that knowledge repo, the normal flow is:
|
|
|
151
141
|
In interactive mode it also:
|
|
152
142
|
|
|
153
143
|
- asks which repo and directory you want to initialize
|
|
154
|
-
- checks the environment values already available
|
|
155
144
|
- asks whether to wire GitHub setup now
|
|
156
145
|
- asks whether to verify the database now
|
|
146
|
+
- collects embedding settings if you want remote embeddings
|
|
157
147
|
- prints the next actions after initialization
|
|
158
148
|
|
|
159
149
|
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,14 +7,12 @@ import {
|
|
|
7
7
|
tryResolveKnowledgeRoot
|
|
8
8
|
} from "./config.js";
|
|
9
9
|
import { connect, initDb, schemaStatus } from "./db.js";
|
|
10
|
-
import { loadDotEnv } from "./dotenv.js";
|
|
11
10
|
import { ingestDocuments } from "./index.js";
|
|
12
11
|
import { writeProposalDocument } from "./kb-proposals.js";
|
|
13
12
|
import { databaseHelp, formatCliError, maskConnection, parseFlags } from "./cli-common.js";
|
|
14
13
|
|
|
15
14
|
export async function main(argv) {
|
|
16
15
|
try {
|
|
17
|
-
loadDotEnv();
|
|
18
16
|
const [command, ...rest] = argv;
|
|
19
17
|
if (!command || command === "--help" || command === "-h") {
|
|
20
18
|
printHelp();
|
package/lib/cli.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
tryResolveKnowledgeRoot
|
|
9
9
|
} from "./config.js";
|
|
10
10
|
import { connect, initDb } from "./db.js";
|
|
11
|
-
import { loadDotEnv } from "./dotenv.js";
|
|
12
11
|
import { ingestDocuments } from "./index.js";
|
|
13
12
|
import { collectInitRepoInteractiveOptions } from "./init-repo-interactive.js";
|
|
14
13
|
import { createGitHubIssueFromText } from "./kb-proposals.js";
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
|
|
26
25
|
export async function main(argv) {
|
|
27
26
|
try {
|
|
28
|
-
loadDotEnv();
|
|
29
27
|
const [command, ...rest] = argv;
|
|
30
28
|
if (!command || command === "--help" || command === "-h") {
|
|
31
29
|
printHelp();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import readline from "node:readline/promises";
|
|
4
5
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
5
6
|
import { resolveGitHubRepository } from "./config.js";
|
|
@@ -10,7 +11,8 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
10
11
|
cwd = process.cwd(),
|
|
11
12
|
stdin = defaultStdin,
|
|
12
13
|
stdout = defaultStdout,
|
|
13
|
-
prompter = null
|
|
14
|
+
prompter = null,
|
|
15
|
+
auth = defaultInteractiveGitHubAuth
|
|
14
16
|
}) {
|
|
15
17
|
if (!prompter && (!stdin.isTTY || !stdout.isTTY)) {
|
|
16
18
|
throw new Error("interactive init-repo requires a TTY; rerun in a terminal or pass flags directly");
|
|
@@ -31,7 +33,6 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
31
33
|
})
|
|
32
34
|
);
|
|
33
35
|
const layout = await askKnowledgeLayout(prompt, targetDir, defaults.layout);
|
|
34
|
-
printEnvCapabilities(stdout, defaults);
|
|
35
36
|
|
|
36
37
|
const configureRepo = await prompt.askConfirm(
|
|
37
38
|
"configure the GitHub repo now",
|
|
@@ -44,8 +45,13 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
44
45
|
validate: requireValue
|
|
45
46
|
});
|
|
46
47
|
if (!defaults.githubToken) {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const authorized = await ensureInteractiveGitHubAccess({ prompt, stdout, auth });
|
|
49
|
+
if (authorized) {
|
|
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
|
+
}
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -54,21 +60,62 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
54
60
|
Boolean(defaults.databaseUrl)
|
|
55
61
|
);
|
|
56
62
|
|
|
57
|
-
let databaseUrl =
|
|
58
|
-
if (verifyDatabase
|
|
59
|
-
|
|
63
|
+
let databaseUrl = null;
|
|
64
|
+
if (verifyDatabase) {
|
|
65
|
+
databaseUrl = await prompt.askText("database URL", defaults.databaseUrl, {
|
|
66
|
+
validate: requireValue
|
|
67
|
+
});
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
let embeddingMode = null;
|
|
71
|
+
let embeddingApiUrl = null;
|
|
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
|
+
}
|
|
64
104
|
}
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
let repoAutomationToken = null;
|
|
107
|
+
if (configureRepo) {
|
|
108
|
+
const useAutomationToken = await prompt.askConfirm(
|
|
109
|
+
"use a custom automation token instead of the default GitHub Actions token",
|
|
110
|
+
Boolean(defaults.repoAutomationToken)
|
|
111
|
+
);
|
|
112
|
+
if (useAutomationToken) {
|
|
113
|
+
repoAutomationToken = await prompt.askText(
|
|
114
|
+
"repo automation token",
|
|
115
|
+
defaults.repoAutomationToken
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
72
119
|
|
|
73
120
|
stdout.write("\nPlan\n");
|
|
74
121
|
stdout.write(` target directory: ${targetDir}\n`);
|
|
@@ -155,6 +202,9 @@ function createReadlinePrompter({ stdin, stdout }) {
|
|
|
155
202
|
askText(label, defaultValue = "", options = {}) {
|
|
156
203
|
return askText(rl, label, defaultValue, options);
|
|
157
204
|
},
|
|
205
|
+
askSecret(label, options = {}) {
|
|
206
|
+
return askSecret({ stdin, stdout, label, ...options });
|
|
207
|
+
},
|
|
158
208
|
askConfirm(label, defaultValue = false) {
|
|
159
209
|
return askConfirm(rl, label, defaultValue);
|
|
160
210
|
},
|
|
@@ -198,6 +248,20 @@ async function askConfirm(rl, label, defaultValue = false) {
|
|
|
198
248
|
}
|
|
199
249
|
}
|
|
200
250
|
|
|
251
|
+
async function askSecret({ stdin, stdout, label, validate = null }) {
|
|
252
|
+
while (true) {
|
|
253
|
+
const value = await readHiddenInput({ stdin, stdout, prompt: `${label}: ` });
|
|
254
|
+
if (!validate) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
const error = validate(value);
|
|
258
|
+
if (!error) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
stdout.write(`${error}\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
201
265
|
function requireValue(value) {
|
|
202
266
|
return String(value ?? "").trim() ? null : "a value is required";
|
|
203
267
|
}
|
|
@@ -218,12 +282,108 @@ function emptyToNull(value) {
|
|
|
218
282
|
return normalized ? normalized : null;
|
|
219
283
|
}
|
|
220
284
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
);
|
|
285
|
+
async function ensureInteractiveGitHubAccess({ prompt, stdout, auth }) {
|
|
286
|
+
const existingToken = auth.getToken();
|
|
287
|
+
if (existingToken) {
|
|
288
|
+
stdout.write("using existing GitHub CLI authentication for repo setup.\n");
|
|
289
|
+
return existingToken;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
stdout.write("\nGitHub repo setup needs GitHub authentication.\n");
|
|
293
|
+
stdout.write("You can paste a GitHub token now, use GitHub CLI browser login, or skip repo setup for now.\n");
|
|
294
|
+
|
|
295
|
+
while (true) {
|
|
296
|
+
const method = (
|
|
297
|
+
await prompt.askText("authentication method (token/login/skip)", "token", {
|
|
298
|
+
validate: validateAuthMethod
|
|
299
|
+
})
|
|
300
|
+
).toLowerCase();
|
|
301
|
+
if (method === "skip") {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (method === "token") {
|
|
305
|
+
const token = await prompt.askSecret("GITHUB_TOKEN", {
|
|
306
|
+
validate: requireValue
|
|
307
|
+
});
|
|
308
|
+
if (token) {
|
|
309
|
+
stdout.write("using the provided GitHub token for repo setup.\n");
|
|
310
|
+
return token;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
auth.login();
|
|
315
|
+
const token = auth.getToken();
|
|
316
|
+
if (token) {
|
|
317
|
+
stdout.write("GitHub CLI authentication is ready.\n");
|
|
318
|
+
return token;
|
|
319
|
+
}
|
|
320
|
+
stdout.write("GitHub CLI login did not yield a usable token.\n");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const defaultInteractiveGitHubAuth = {
|
|
325
|
+
getToken() {
|
|
326
|
+
try {
|
|
327
|
+
return execFileSync("gh", ["auth", "token"], {
|
|
328
|
+
encoding: "utf8",
|
|
329
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
330
|
+
}).trim();
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
login() {
|
|
336
|
+
execFileSync("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
|
|
337
|
+
stdio: "inherit"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
function validateAuthMethod(value) {
|
|
343
|
+
if (["token", "login", "skip"].includes(String(value ?? "").trim().toLowerCase())) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
return "enter token, login, or skip";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function readHiddenInput({ stdin, stdout, prompt }) {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
stdout.write(prompt);
|
|
352
|
+
let value = "";
|
|
353
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
354
|
+
|
|
355
|
+
const cleanup = () => {
|
|
356
|
+
stdin.off("data", onData);
|
|
357
|
+
if (typeof stdin.setRawMode === "function") {
|
|
358
|
+
stdin.setRawMode(wasRaw);
|
|
359
|
+
}
|
|
360
|
+
stdout.write("\n");
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const onData = (chunk) => {
|
|
364
|
+
const text = chunk.toString("utf8");
|
|
365
|
+
for (const char of text) {
|
|
366
|
+
if (char === "\u0003") {
|
|
367
|
+
cleanup();
|
|
368
|
+
reject(new Error("interactive init-repo cancelled"));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (char === "\r" || char === "\n") {
|
|
372
|
+
cleanup();
|
|
373
|
+
resolve(value.trim());
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (char === "\u007f") {
|
|
377
|
+
value = value.slice(0, -1);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
value += char;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (typeof stdin.setRawMode === "function") {
|
|
385
|
+
stdin.setRawMode(true);
|
|
386
|
+
}
|
|
387
|
+
stdin.on("data", onData);
|
|
388
|
+
});
|
|
229
389
|
}
|
package/lib/repo-init.js
CHANGED
|
@@ -28,7 +28,6 @@ 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()],
|
|
32
31
|
[".github/ISSUE_TEMPLATE/config.yml", renderIssueConfig()],
|
|
33
32
|
[".github/ISSUE_TEMPLATE/knowledge-document.md", renderIssueTemplate({ docsRootRelative })],
|
|
34
33
|
[".github/workflows/kb-issue-to-pr.yml", renderIssueToPrWorkflow({ docsRootRelative })],
|
|
@@ -361,21 +360,6 @@ function renderIssueConfig() {
|
|
|
361
360
|
return ["blank_issues_enabled: false", ""].join("\n");
|
|
362
361
|
}
|
|
363
362
|
|
|
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
|
-
|
|
379
363
|
function renderIssueTemplate({ docsRootRelative }) {
|
|
380
364
|
return [
|
|
381
365
|
"---",
|
package/package.json
CHANGED
package/lib/dotenv.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
}
|