@kibhq/cli 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.
@@ -0,0 +1,100 @@
1
+ import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
2
+ import * as log from "../ui/logger.js";
3
+ import { createSpinner } from "../ui/spinner.js";
4
+
5
+ interface LintOpts {
6
+ fix?: boolean;
7
+ check?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ export async function lint(opts: LintOpts) {
12
+ let root: string;
13
+ try {
14
+ root = resolveVaultRoot();
15
+ } catch (err) {
16
+ if (err instanceof VaultNotFoundError) {
17
+ log.error(err.message);
18
+ process.exit(1);
19
+ }
20
+ throw err;
21
+ }
22
+
23
+ const { lintVault } = await import("@kibhq/core");
24
+
25
+ log.header("linting wiki");
26
+
27
+ const spinner = createSpinner("Checking articles...");
28
+ spinner.start();
29
+
30
+ const result = await lintVault(root, {
31
+ ruleFilter: opts.check,
32
+ onProgress: (msg) => {
33
+ spinner.text = ` ${msg}`;
34
+ },
35
+ });
36
+
37
+ spinner.stop();
38
+
39
+ if (opts.json) {
40
+ console.log(JSON.stringify(result, null, 2));
41
+ return;
42
+ }
43
+
44
+ const total = result.diagnostics.length;
45
+ if (total === 0) {
46
+ log.success("No issues found");
47
+ log.blank();
48
+ return;
49
+ }
50
+
51
+ console.log(` ${total} issue${total === 1 ? "" : "s"} found:`);
52
+ log.blank();
53
+
54
+ const severityLabel = {
55
+ error: "\x1b[31mERROR\x1b[0m ",
56
+ warning: "\x1b[33mWARN\x1b[0m ",
57
+ info: "\x1b[36mINFO\x1b[0m ",
58
+ };
59
+
60
+ const ruleLabel = {
61
+ orphan: "ORPHAN ",
62
+ "broken-link": "LINK ",
63
+ stale: "STALE ",
64
+ frontmatter: "FMATTER",
65
+ missing: "MISSING",
66
+ };
67
+
68
+ for (const d of result.diagnostics) {
69
+ const sev = severityLabel[d.severity];
70
+ const rule = ruleLabel[d.rule] ?? d.rule.padEnd(7);
71
+ const path = d.path ? `\x1b[2m${d.path}\x1b[0m` : "";
72
+
73
+ console.log(` ${sev} ${rule} ${d.message}`);
74
+ if (path) {
75
+ console.log(` ${path}`);
76
+ }
77
+ console.log();
78
+ }
79
+
80
+ // Summary
81
+ const parts: string[] = [];
82
+ if (result.errors > 0) parts.push(`${result.errors} error${result.errors === 1 ? "" : "s"}`);
83
+ if (result.warnings > 0)
84
+ parts.push(`${result.warnings} warning${result.warnings === 1 ? "" : "s"}`);
85
+ if (result.infos > 0) parts.push(`${result.infos} info${result.infos === 1 ? "" : "s"}`);
86
+ log.dim(parts.join(", "));
87
+
88
+ const fixable = result.diagnostics.filter((d) => d.fixable).length;
89
+ if (fixable > 0 && !opts.fix) {
90
+ log.blank();
91
+ log.dim(`${fixable} fixable issue${fixable === 1 ? "" : "s"} — run kib lint --fix`);
92
+ }
93
+
94
+ log.blank();
95
+
96
+ // Exit with error code if there are errors
97
+ if (result.errors > 0) {
98
+ process.exit(1);
99
+ }
100
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ createProvider,
3
+ loadConfig,
4
+ NoProviderError,
5
+ resolveVaultRoot,
6
+ VaultNotFoundError,
7
+ } from "@kibhq/core";
8
+ import * as log from "../ui/logger.js";
9
+ import { setupProvider } from "../ui/setup-provider.js";
10
+ import { createSpinner } from "../ui/spinner.js";
11
+
12
+ interface QueryOpts {
13
+ file?: boolean;
14
+ sources?: boolean;
15
+ json?: boolean;
16
+ }
17
+
18
+ export async function query(question: string, opts: QueryOpts) {
19
+ let root: string;
20
+ try {
21
+ root = resolveVaultRoot();
22
+ } catch (err) {
23
+ if (err instanceof VaultNotFoundError) {
24
+ log.error(err.message);
25
+ process.exit(1);
26
+ }
27
+ throw err;
28
+ }
29
+
30
+ const config = await loadConfig(root);
31
+
32
+ // Create provider
33
+ let provider;
34
+ try {
35
+ provider = await createProvider(config.provider.default, config.provider.model);
36
+ } catch (err) {
37
+ if (err instanceof NoProviderError) {
38
+ provider = await setupProvider(root);
39
+ } else {
40
+ log.error((err as Error).message);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ const { queryVault } = await import("@kibhq/core");
46
+
47
+ log.header("querying knowledge base");
48
+
49
+ const spinner = createSpinner("Searching and generating answer...");
50
+ spinner.start();
51
+
52
+ try {
53
+ const result = await queryVault(root, question, provider);
54
+ spinner.stop();
55
+
56
+ // Print answer
57
+ console.log();
58
+ console.log(result.answer);
59
+ console.log();
60
+
61
+ // Print sources if requested
62
+ if (opts.sources && result.sourcePaths.length > 0) {
63
+ log.dim("Sources:");
64
+ for (const path of result.sourcePaths) {
65
+ log.dim(` - ${path}`);
66
+ }
67
+ log.blank();
68
+ }
69
+
70
+ if (opts.json) {
71
+ console.log(JSON.stringify(result, null, 2));
72
+ }
73
+ } catch (err) {
74
+ spinner.fail("Query failed");
75
+ log.error((err as Error).message);
76
+ process.exit(1);
77
+ }
78
+ }
@@ -0,0 +1,87 @@
1
+ import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
2
+ import * as log from "../ui/logger.js";
3
+ import { createSpinner } from "../ui/spinner.js";
4
+
5
+ interface SearchOpts {
6
+ wiki?: boolean;
7
+ raw?: boolean;
8
+ limit?: number;
9
+ json?: boolean;
10
+ }
11
+
12
+ export async function search(term: string, opts: SearchOpts) {
13
+ let root: string;
14
+ try {
15
+ root = resolveVaultRoot();
16
+ } catch (err) {
17
+ if (err instanceof VaultNotFoundError) {
18
+ log.error(err.message);
19
+ process.exit(1);
20
+ }
21
+ throw err;
22
+ }
23
+
24
+ const { SearchIndex } = await import("@kibhq/core");
25
+
26
+ const scope = opts.wiki ? "wiki" : opts.raw ? "raw" : "all";
27
+ const limit = opts.limit ?? 20;
28
+
29
+ const spinner = createSpinner("Searching...");
30
+ spinner.start();
31
+
32
+ const index = new SearchIndex();
33
+
34
+ // Try to load cached index first
35
+ const loaded = await index.load(root);
36
+ if (!loaded) {
37
+ spinner.text = " Building search index...";
38
+ await index.build(root, scope);
39
+ await index.save(root);
40
+ }
41
+
42
+ const start = performance.now();
43
+ const results = index.search(term, { limit });
44
+ const elapsed = Math.round(performance.now() - start);
45
+
46
+ spinner.stop();
47
+
48
+ if (opts.json) {
49
+ console.log(JSON.stringify(results, null, 2));
50
+ return;
51
+ }
52
+
53
+ log.header("searching vault");
54
+
55
+ if (results.length === 0) {
56
+ log.dim(`No results for "${term}"`);
57
+ log.blank();
58
+ return;
59
+ }
60
+
61
+ console.log(` ${results.length} result${results.length === 1 ? "" : "s"} (${elapsed}ms):`);
62
+ log.blank();
63
+
64
+ for (let i = 0; i < results.length; i++) {
65
+ const r = results[i]!;
66
+ const num = String(i + 1).padStart(2);
67
+ const title = r.title ?? r.path.split("/").pop()?.replace(/\.md$/, "") ?? r.path;
68
+ const score = r.score.toFixed(2).padStart(5);
69
+
70
+ console.log(` ${num}. ${title} ${score}`);
71
+ console.log(` ${dimPath(r.path)}`);
72
+ if (r.snippet) {
73
+ console.log(` ${truncate(r.snippet, 80)}`);
74
+ }
75
+ console.log();
76
+ }
77
+ }
78
+
79
+ function dimPath(path: string): string {
80
+ // Import chalk dynamically to keep lazy loading
81
+ return `\x1b[2m${path}\x1b[0m`;
82
+ }
83
+
84
+ function truncate(str: string, max: number): string {
85
+ if (str.length <= max) return str;
86
+ return `${str.slice(0, max - 3)}...`;
87
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ createProvider,
3
+ loadConfig,
4
+ NoProviderError,
5
+ resolveVaultRoot,
6
+ VaultNotFoundError,
7
+ } from "@kibhq/core";
8
+ import * as log from "../ui/logger.js";
9
+ import { setupProvider } from "../ui/setup-provider.js";
10
+ import { createSpinner } from "../ui/spinner.js";
11
+
12
+ export async function skill(subcommand: string, name?: string, _opts?: unknown) {
13
+ let root: string;
14
+ try {
15
+ root = resolveVaultRoot();
16
+ } catch (err) {
17
+ if (err instanceof VaultNotFoundError) {
18
+ log.error(err.message);
19
+ process.exit(1);
20
+ }
21
+ throw err;
22
+ }
23
+
24
+ const { loadSkills, findSkill, runSkill } = await import("@kibhq/core");
25
+
26
+ switch (subcommand) {
27
+ case "list": {
28
+ log.header("available skills");
29
+
30
+ const skills = await loadSkills(root);
31
+
32
+ for (const s of skills) {
33
+ console.log(` ${s.name.padEnd(20)} ${s.description}`);
34
+ }
35
+ log.blank();
36
+ log.dim(`Run a skill: kib skill run <name>`);
37
+ log.blank();
38
+ break;
39
+ }
40
+
41
+ case "run": {
42
+ if (!name) {
43
+ log.error("Skill name required. Usage: kib skill run <name>");
44
+ process.exit(1);
45
+ }
46
+
47
+ const s = await findSkill(root, name);
48
+ if (!s) {
49
+ log.error(`Skill "${name}" not found. Run kib skill list to see available skills.`);
50
+ process.exit(1);
51
+ }
52
+
53
+ log.header(`running skill: ${s.name}`);
54
+
55
+ let provider;
56
+ if (s.llm?.required) {
57
+ const config = await loadConfig(root);
58
+ const modelKey = s.llm.model === "fast" ? "fast_model" : "model";
59
+ const model = config.provider[modelKey];
60
+ try {
61
+ provider = await createProvider(config.provider.default, model);
62
+ } catch (err) {
63
+ if (err instanceof NoProviderError) {
64
+ provider = await setupProvider(root);
65
+ } else {
66
+ log.error((err as Error).message);
67
+ process.exit(1);
68
+ }
69
+ }
70
+ }
71
+
72
+ const spinner = createSpinner(`Running ${s.name}...`);
73
+ spinner.start();
74
+
75
+ try {
76
+ const result = await runSkill(root, s, { provider });
77
+ spinner.succeed(`${s.name} completed`);
78
+
79
+ if (result.content) {
80
+ log.blank();
81
+ console.log(result.content);
82
+ log.blank();
83
+ }
84
+ } catch (err) {
85
+ spinner.fail(`${s.name} failed`);
86
+ log.error((err as Error).message);
87
+ process.exit(1);
88
+ }
89
+ break;
90
+ }
91
+
92
+ default:
93
+ log.error(`Unknown subcommand: ${subcommand}`);
94
+ log.dim("Available: list, run");
95
+ process.exit(1);
96
+ }
97
+ }
@@ -0,0 +1,70 @@
1
+ import { loadConfig, loadManifest, resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
2
+ import * as log from "../ui/logger.js";
3
+
4
+ export async function status() {
5
+ let root: string;
6
+ try {
7
+ root = resolveVaultRoot();
8
+ } catch (err) {
9
+ if (err instanceof VaultNotFoundError) {
10
+ log.error(err.message);
11
+ process.exit(1);
12
+ }
13
+ throw err;
14
+ }
15
+
16
+ const manifest = await loadManifest(root);
17
+ const config = await loadConfig(root);
18
+
19
+ log.header("vault status");
20
+
21
+ log.keyValue("vault", manifest.vault.name);
22
+ log.keyValue("provider", `${config.provider.default} (${config.provider.model})`);
23
+ log.keyValue("path", root);
24
+ log.blank();
25
+
26
+ const sourceCount = Object.keys(manifest.sources).length;
27
+ const articleCount = Object.keys(manifest.articles).length;
28
+ const pendingCount = Object.values(manifest.sources).filter(
29
+ (s) => !s.lastCompiled || s.lastCompiled < s.ingestedAt,
30
+ ).length;
31
+
32
+ log.keyValue(
33
+ "SOURCES",
34
+ `${sourceCount} total${pendingCount > 0 ? ` | ${pendingCount} pending compilation` : ""}`,
35
+ );
36
+ log.keyValue(
37
+ "ARTICLES",
38
+ `${articleCount} total | ${manifest.stats.totalWords.toLocaleString()} words`,
39
+ );
40
+
41
+ if (manifest.vault.lastCompiled) {
42
+ const ago = timeAgo(new Date(manifest.vault.lastCompiled));
43
+ log.keyValue("LAST COMPILED", ago);
44
+ } else {
45
+ log.keyValue("LAST COMPILED", "never");
46
+ }
47
+
48
+ if (manifest.stats.lastLintAt) {
49
+ const ago = timeAgo(new Date(manifest.stats.lastLintAt));
50
+ log.keyValue("LAST LINT", ago);
51
+ }
52
+
53
+ if (pendingCount > 0) {
54
+ log.blank();
55
+ log.warn(`${pendingCount} sources pending — run kib compile`);
56
+ }
57
+
58
+ log.blank();
59
+ }
60
+
61
+ function timeAgo(date: Date): string {
62
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
63
+ if (seconds < 60) return "just now";
64
+ const minutes = Math.floor(seconds / 60);
65
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
66
+ const hours = Math.floor(minutes / 60);
67
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
68
+ const days = Math.floor(hours / 24);
69
+ return `${days} day${days === 1 ? "" : "s"} ago`;
70
+ }
@@ -0,0 +1,141 @@
1
+ import { watch as fsWatch } from "node:fs";
2
+ import { readdir, stat } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { INBOX_DIR, loadConfig, resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
5
+ import * as log from "../ui/logger.js";
6
+
7
+ export async function watch() {
8
+ let root: string;
9
+ try {
10
+ root = resolveVaultRoot();
11
+ } catch (err) {
12
+ if (err instanceof VaultNotFoundError) {
13
+ log.error(err.message);
14
+ process.exit(1);
15
+ }
16
+ throw err;
17
+ }
18
+
19
+ const config = await loadConfig(root);
20
+ const inboxPath = resolve(root, config.watch.inbox_path);
21
+ const { ingestSource } = await import("@kibhq/core");
22
+
23
+ log.header(`watching ${config.watch.inbox_path}/`);
24
+ log.dim(`Drop files into ${inboxPath} to auto-ingest.`);
25
+ log.dim("Press Ctrl+C to stop.");
26
+ log.blank();
27
+
28
+ // Track already-seen files to avoid double-processing
29
+ const processed = new Set<string>();
30
+
31
+ // Seed with existing files
32
+ try {
33
+ const existing = await readdir(inboxPath);
34
+ for (const f of existing) processed.add(f);
35
+ } catch {
36
+ // inbox might not exist yet
37
+ }
38
+
39
+ // Start the HTTP server for browser extension
40
+ const server = startHttpServer(root, ingestSource);
41
+
42
+ // Watch for new files
43
+ const watcher = fsWatch(inboxPath, { recursive: false }, async (event, filename) => {
44
+ if (!filename || processed.has(filename)) return;
45
+ if (filename.startsWith(".")) return; // skip dotfiles
46
+
47
+ const filePath = join(inboxPath, filename);
48
+
49
+ // Wait briefly for file to finish writing
50
+ await new Promise((r) => setTimeout(r, 500));
51
+
52
+ try {
53
+ await stat(filePath);
54
+ } catch {
55
+ return; // file was deleted before we could process it
56
+ }
57
+
58
+ processed.add(filename);
59
+ const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
60
+
61
+ try {
62
+ log.info(`${timestamp} Ingesting ${filename}...`);
63
+ const result = await ingestSource(root, filePath);
64
+
65
+ if (result.skipped) {
66
+ log.dim(`${timestamp} Skipped: ${result.skipReason}`);
67
+ } else {
68
+ log.success(`${timestamp} ${result.title} → ${result.path}`);
69
+
70
+ if (config.watch.auto_compile) {
71
+ log.dim(`${timestamp} Auto-compile: run kib compile to process`);
72
+ }
73
+ }
74
+ } catch (err) {
75
+ log.error(`${timestamp} Failed to ingest ${filename}: ${(err as Error).message}`);
76
+ }
77
+ });
78
+
79
+ // Handle graceful shutdown
80
+ process.on("SIGINT", () => {
81
+ watcher.close();
82
+ server?.stop();
83
+ log.blank();
84
+ log.dim("Watch stopped.");
85
+ process.exit(0);
86
+ });
87
+ }
88
+
89
+ function startHttpServer(root: string, ingestSource: typeof import("@kibhq/core").ingestSource) {
90
+ try {
91
+ const server = Bun.serve({
92
+ port: 4747,
93
+ async fetch(req) {
94
+ const url = new URL(req.url);
95
+
96
+ if (req.method === "POST" && url.pathname === "/ingest") {
97
+ try {
98
+ const body = (await req.json()) as { content: string; url?: string; title?: string };
99
+
100
+ // Write content to a temp file in inbox
101
+ const slug = (body.title ?? "untitled")
102
+ .toLowerCase()
103
+ .replace(/[^a-z0-9]+/g, "-")
104
+ .slice(0, 60);
105
+ const tmpPath = join(root, "inbox", `${slug}-${Date.now()}.md`);
106
+
107
+ const fullContent = body.title
108
+ ? `# ${body.title}\n\n${body.url ? `Source: ${body.url}\n\n` : ""}${body.content}`
109
+ : body.content;
110
+
111
+ await Bun.write(tmpPath, fullContent);
112
+ await ingestSource(root, tmpPath, { title: body.title });
113
+
114
+ return new Response(JSON.stringify({ ok: true }), {
115
+ headers: { "Content-Type": "application/json" },
116
+ });
117
+ } catch (err) {
118
+ return new Response(JSON.stringify({ error: (err as Error).message }), {
119
+ status: 500,
120
+ headers: { "Content-Type": "application/json" },
121
+ });
122
+ }
123
+ }
124
+
125
+ if (req.method === "GET" && url.pathname === "/") {
126
+ return new Response("kib watch running", {
127
+ headers: { "Content-Type": "text/plain" },
128
+ });
129
+ }
130
+
131
+ return new Response("Not found", { status: 404 });
132
+ },
133
+ });
134
+
135
+ log.dim(`HTTP server listening on http://localhost:4747`);
136
+ return server;
137
+ } catch {
138
+ log.dim("HTTP server not started (port 4747 may be in use)");
139
+ return null;
140
+ }
141
+ }
package/src/index.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { Command } from "commander";
2
+
3
+ const program = new Command()
4
+ .name("kib")
5
+ .description("The Headless Knowledge Compiler")
6
+ .version("0.1.0");
7
+
8
+ program
9
+ .command("init")
10
+ .description("Create a new vault in the current directory")
11
+ .option("--name <name>", "vault name (default: directory name)")
12
+ .option("--provider <provider>", "force LLM provider instead of auto-detect")
13
+ .action(async (opts) => {
14
+ const { init } = await import("./commands/init.js");
15
+ await init(opts);
16
+ });
17
+
18
+ program
19
+ .command("config [key] [value]")
20
+ .description("Get or set configuration")
21
+ .option("--list", "list all configuration")
22
+ .action(async (key, value, opts) => {
23
+ const { config } = await import("./commands/config.js");
24
+ await config(key, value, opts);
25
+ });
26
+
27
+ program
28
+ .command("status")
29
+ .description("Vault health dashboard")
30
+ .action(async () => {
31
+ const { status } = await import("./commands/status.js");
32
+ await status();
33
+ });
34
+
35
+ program
36
+ .command("ingest <sources...>")
37
+ .description("Ingest sources into raw/")
38
+ .option("--category <cat>", "override raw/ subdirectory")
39
+ .option("--tags <tags>", "comma-separated tags")
40
+ .option("--batch", "read sources from stdin (one per line)")
41
+ .action(async (sources, opts) => {
42
+ const { ingest } = await import("./commands/ingest.js");
43
+ await ingest(sources, opts);
44
+ });
45
+
46
+ program
47
+ .command("compile")
48
+ .description("Compile raw sources into wiki articles")
49
+ .option("--force", "recompile all sources")
50
+ .option("--dry-run", "show what would happen without doing it")
51
+ .option("--source <path>", "compile only a specific source")
52
+ .option("--max <n>", "limit sources per pass", Number.parseInt)
53
+ .action(async (opts) => {
54
+ const { compile } = await import("./commands/compile.js");
55
+ await compile(opts);
56
+ });
57
+
58
+ program
59
+ .command("search <term>")
60
+ .description("Fast text search across the vault")
61
+ .option("--wiki", "search only wiki/")
62
+ .option("--raw", "search only raw/")
63
+ .option("--limit <n>", "max results", Number.parseInt)
64
+ .option("--json", "JSON output")
65
+ .action(async (term, opts) => {
66
+ const { search } = await import("./commands/search.js");
67
+ await search(term, opts);
68
+ });
69
+
70
+ program
71
+ .command("query <question>")
72
+ .description("Ask a question against the knowledge base")
73
+ .option("--file", "auto-file to wiki/outputs/")
74
+ .option("--no-file", "never file")
75
+ .option("--sources", "show which articles were used")
76
+ .option("--json", "JSON output")
77
+ .action(async (question, opts) => {
78
+ const { query } = await import("./commands/query.js");
79
+ await query(question, opts);
80
+ });
81
+
82
+ program
83
+ .command("chat")
84
+ .description("Interactive REPL with the knowledge base")
85
+ .action(async () => {
86
+ const { chat } = await import("./commands/chat.js");
87
+ await chat();
88
+ });
89
+
90
+ program
91
+ .command("lint")
92
+ .description("Run health checks on the wiki")
93
+ .option("--fix", "auto-fix all issues")
94
+ .option("--check <type>", "run specific check")
95
+ .option("--json", "JSON output")
96
+ .action(async (opts) => {
97
+ const { lint } = await import("./commands/lint.js");
98
+ await lint(opts);
99
+ });
100
+
101
+ program
102
+ .command("skill <subcommand> [name]")
103
+ .description("Manage skills (install, list, run, create)")
104
+ .action(async (subcommand, name, opts) => {
105
+ const { skill } = await import("./commands/skill.js");
106
+ await skill(subcommand, name, opts);
107
+ });
108
+
109
+ program
110
+ .command("watch")
111
+ .description("Watch inbox/ and auto-ingest")
112
+ .action(async () => {
113
+ const { watch } = await import("./commands/watch.js");
114
+ await watch();
115
+ });
116
+
117
+ program
118
+ .command("export")
119
+ .description("Export wiki to other formats")
120
+ .option("--format <type>", "output format: markdown, html, pdf", "markdown")
121
+ .option("--output <path>", "output directory")
122
+ .action(async (opts) => {
123
+ const { exportVault } = await import("./commands/export.js");
124
+ await exportVault(opts);
125
+ });
126
+
127
+ export function main() {
128
+ program.parse();
129
+ }