@nzpr/kb 0.1.5 → 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 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
- 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
- }
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
- databaseUrl = await prompt.askText("database URL", defaults.databaseUrl, {
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
- 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
- }
62
+ if (!databaseUrl) {
63
+ databaseUrl = null;
104
64
  }
105
65
 
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
- }
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`);
@@ -202,9 +155,6 @@ function createReadlinePrompter({ stdin, stdout }) {
202
155
  askText(label, defaultValue = "", options = {}) {
203
156
  return askText(rl, label, defaultValue, options);
204
157
  },
205
- askSecret(label, options = {}) {
206
- return askSecret({ stdin, stdout, label, ...options });
207
- },
208
158
  askConfirm(label, defaultValue = false) {
209
159
  return askConfirm(rl, label, defaultValue);
210
160
  },
@@ -248,20 +198,6 @@ async function askConfirm(rl, label, defaultValue = false) {
248
198
  }
249
199
  }
250
200
 
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
-
265
201
  function requireValue(value) {
266
202
  return String(value ?? "").trim() ? null : "a value is required";
267
203
  }
@@ -282,108 +218,12 @@ function emptyToNull(value) {
282
218
  return normalized ? normalized : null;
283
219
  }
284
220
 
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
- });
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
+ );
389
229
  }
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
  "---",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nzpr/kb",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Knowledge base CLI for proposing, publishing, and querying curated agent knowledge.",
5
5
  "repository": {
6
6
  "type": "git",