@kibhq/cli 0.2.0 → 0.3.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/README.md CHANGED
@@ -7,7 +7,7 @@ The Headless Knowledge Compiler. A CLI-first, LLM-powered tool that turns raw so
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- # npm / bun
10
+ # Requires Bun (https://bun.sh)
11
11
  npm i -g @kibhq/cli
12
12
 
13
13
  # or run without installing
@@ -53,10 +53,13 @@ CORE
53
53
  lint Run health checks on the wiki
54
54
  status Vault health dashboard
55
55
 
56
+ INTEGRATION
57
+ serve --mcp Start MCP server for AI tool integration
58
+ watch Watch inbox/ and auto-ingest new files
59
+
56
60
  MANAGEMENT
57
61
  config [key] [val] Get or set configuration
58
62
  skill <sub> [name] Manage skills (list, run)
59
- watch Watch inbox/ and auto-ingest new files
60
63
  export Export wiki to markdown or HTML
61
64
  ```
62
65
 
@@ -92,6 +95,30 @@ my-vault/
92
95
 
93
96
  The vault is just files. View it in any editor. Version it with git. No lock-in.
94
97
 
98
+ ## MCP Server
99
+
100
+ Expose your vault as MCP tools for AI tool integration:
101
+
102
+ ```bash
103
+ kib serve --mcp
104
+ ```
105
+
106
+ 8 tools: `kib_status`, `kib_list`, `kib_read`, `kib_search`, `kib_query`, `kib_ingest`, `kib_compile`, `kib_lint`
107
+
108
+ Add to your MCP client config:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "kib": {
114
+ "command": "kib",
115
+ "args": ["serve", "--mcp"],
116
+ "cwd": "/path/to/your/vault"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
95
122
  ## Links
96
123
 
97
124
  - [GitHub](https://github.com/keeganthomp/kib)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kibhq/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Headless Knowledge Compiler — turn raw sources into a structured, queryable wiki with AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,8 +36,9 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@kibhq/core": "^0.1.0",
39
- "commander": "^14.0.0",
39
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
40
  "chalk": "^5.4.1",
41
+ "commander": "^14.0.0",
41
42
  "ora": "^8.2.0"
42
43
  },
43
44
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  import * as readline from "node:readline";
2
- import type { Message } from "@kibhq/core";
2
+ import type { LLMProvider, Message } from "@kibhq/core";
3
3
  import {
4
4
  createProvider,
5
5
  loadConfig,
@@ -25,7 +25,7 @@ export async function chat() {
25
25
  const config = await loadConfig(root);
26
26
 
27
27
  // Create provider
28
- let provider;
28
+ let provider: LLMProvider;
29
29
  try {
30
30
  provider = await createProvider(config.provider.default, config.provider.model);
31
31
  } catch (err) {
@@ -1,3 +1,4 @@
1
+ import type { LLMProvider } from "@kibhq/core";
1
2
  import {
2
3
  createProvider,
3
4
  loadConfig,
@@ -33,7 +34,7 @@ export async function compile(opts: CompileOpts) {
33
34
  log.header("compiling wiki");
34
35
 
35
36
  // Create LLM provider
36
- let provider;
37
+ let provider: LLMProvider;
37
38
  const providerSpinner = createSpinner("Connecting to LLM provider...");
38
39
  providerSpinner.start();
39
40
  try {
@@ -45,28 +45,32 @@ export async function config(key?: string, value?: string, opts?: { list?: boole
45
45
  }
46
46
  }
47
47
 
48
- function getNestedValue(obj: any, path: string): unknown {
48
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
49
49
  const parts = path.split(".");
50
- let current = obj;
50
+ let current: unknown = obj;
51
51
  for (const part of parts) {
52
52
  if (current == null || typeof current !== "object") return undefined;
53
- current = current[part];
53
+ current = (current as Record<string, unknown>)[part];
54
54
  }
55
55
  return current;
56
56
  }
57
57
 
58
- function setNestedValue(obj: any, path: string, value: unknown): boolean {
58
+ function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): boolean {
59
59
  const parts = path.split(".");
60
- let current = obj;
60
+ let current: unknown = obj;
61
61
  for (let i = 0; i < parts.length - 1; i++) {
62
62
  if (current == null || typeof current !== "object") return false;
63
- current = current[parts[i]!];
63
+ current = (current as Record<string, unknown>)[parts[i]!];
64
64
  }
65
65
  const lastKey = parts[parts.length - 1]!;
66
- if (current == null || typeof current !== "object" || !(lastKey in current)) {
66
+ if (
67
+ current == null ||
68
+ typeof current !== "object" ||
69
+ !(lastKey in (current as Record<string, unknown>))
70
+ ) {
67
71
  return false;
68
72
  }
69
- current[lastKey] = value;
73
+ (current as Record<string, unknown>)[lastKey] = value;
70
74
  return true;
71
75
  }
72
76
 
@@ -78,7 +82,7 @@ function parseValue(val: string): unknown {
78
82
  return val;
79
83
  }
80
84
 
81
- function printConfig(obj: any, prefix: string) {
85
+ function printConfig(obj: Record<string, unknown>, prefix: string) {
82
86
  for (const [key, val] of Object.entries(obj)) {
83
87
  const fullKey = prefix ? `${prefix}.${key}` : key;
84
88
  if (val != null && typeof val === "object" && !Array.isArray(val)) {
@@ -19,7 +19,7 @@ export async function init(opts: InitOpts) {
19
19
  const provider = opts.provider ?? detected.name;
20
20
  const model = detected.model;
21
21
 
22
- const { root, manifest } = await initVault(cwd, {
22
+ await initVault(cwd, {
23
23
  name: opts.name,
24
24
  provider,
25
25
  model,
@@ -1,3 +1,4 @@
1
+ import type { LLMProvider } from "@kibhq/core";
1
2
  import {
2
3
  createProvider,
3
4
  loadConfig,
@@ -30,7 +31,7 @@ export async function query(question: string, opts: QueryOpts) {
30
31
  const config = await loadConfig(root);
31
32
 
32
33
  // Create provider
33
- let provider;
34
+ let provider: LLMProvider;
34
35
  try {
35
36
  provider = await createProvider(config.provider.default, config.provider.model);
36
37
  } catch (err) {
@@ -0,0 +1,31 @@
1
+ import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
2
+
3
+ interface ServeOpts {
4
+ mcp?: boolean;
5
+ }
6
+
7
+ export async function serve(opts: ServeOpts) {
8
+ if (!opts.mcp) {
9
+ console.error("Usage: kib serve --mcp");
10
+ console.error(" Starts an MCP server over stdio for AI tool integration.");
11
+ process.exit(1);
12
+ }
13
+
14
+ let root: string;
15
+ try {
16
+ root = resolveVaultRoot();
17
+ } catch (e) {
18
+ if (e instanceof VaultNotFoundError) {
19
+ console.error(e.message);
20
+ process.exit(1);
21
+ }
22
+ throw e;
23
+ }
24
+
25
+ // Load saved API keys before starting server
26
+ const { loadCredentials } = await import("../ui/setup-provider.js");
27
+ loadCredentials();
28
+
29
+ const { startMcpServer } = await import("../mcp/server.js");
30
+ await startMcpServer(root);
31
+ }
@@ -1,3 +1,4 @@
1
+ import type { LLMProvider } from "@kibhq/core";
1
2
  import {
2
3
  createProvider,
3
4
  loadConfig,
@@ -52,7 +53,7 @@ export async function skill(subcommand: string, name?: string, _opts?: unknown)
52
53
 
53
54
  log.header(`running skill: ${s.name}`);
54
55
 
55
- let provider;
56
+ let provider: LLMProvider | undefined;
56
57
  if (s.llm?.required) {
57
58
  const config = await loadConfig(root);
58
59
  const modelKey = s.llm.model === "fast" ? "fast_model" : "model";
@@ -1,7 +1,7 @@
1
1
  import { watch as fsWatch } from "node:fs";
2
2
  import { readdir, stat } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
- import { INBOX_DIR, loadConfig, resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
4
+ import { loadConfig, resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
5
5
  import * as log from "../ui/logger.js";
6
6
 
7
7
  export async function watch() {
@@ -40,7 +40,7 @@ export async function watch() {
40
40
  const server = startHttpServer(root, ingestSource);
41
41
 
42
42
  // Watch for new files
43
- const watcher = fsWatch(inboxPath, { recursive: false }, async (event, filename) => {
43
+ const watcher = fsWatch(inboxPath, { recursive: false }, async (_event, filename) => {
44
44
  if (!filename || processed.has(filename)) return;
45
45
  if (filename.startsWith(".")) return; // skip dotfiles
46
46
 
package/src/index.ts CHANGED
@@ -114,6 +114,15 @@ program
114
114
  await watch();
115
115
  });
116
116
 
117
+ program
118
+ .command("serve")
119
+ .description("Start an MCP server for AI tool integration")
120
+ .option("--mcp", "expose vault as MCP tools over stdio")
121
+ .action(async (opts) => {
122
+ const { serve } = await import("./commands/serve.js");
123
+ await serve(opts);
124
+ });
125
+
117
126
  program
118
127
  .command("export")
119
128
  .description("Export wiki to other formats")
@@ -0,0 +1,332 @@
1
+ import {
2
+ compileVault,
3
+ createProvider,
4
+ ingestSource,
5
+ type LLMProvider,
6
+ lintVault,
7
+ listRaw,
8
+ listWiki,
9
+ loadConfig,
10
+ loadManifest,
11
+ type Manifest,
12
+ queryVault,
13
+ readGraph,
14
+ readIndex,
15
+ readRaw,
16
+ readWiki,
17
+ SearchIndex,
18
+ type VaultConfig,
19
+ } from "@kibhq/core";
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { z } from "zod";
23
+
24
+ // ─── Context ────────────────────────────────────────────────────
25
+
26
+ interface McpContext {
27
+ root: string;
28
+ getConfig(): Promise<VaultConfig>;
29
+ getManifest(): Promise<Manifest>;
30
+ getProvider(): Promise<LLMProvider>;
31
+ getSearchIndex(): Promise<SearchIndex>;
32
+ invalidateSearch(): void;
33
+ }
34
+
35
+ function createContext(root: string): McpContext {
36
+ let cachedConfig: VaultConfig | null = null;
37
+ let cachedProvider: LLMProvider | null = null;
38
+ let cachedIndex: SearchIndex | null = null;
39
+
40
+ return {
41
+ root,
42
+ async getConfig() {
43
+ if (!cachedConfig) cachedConfig = await loadConfig(root);
44
+ return cachedConfig;
45
+ },
46
+ async getManifest() {
47
+ // Always re-read — vault mutates
48
+ return loadManifest(root);
49
+ },
50
+ async getProvider() {
51
+ if (!cachedProvider) {
52
+ const config = await this.getConfig();
53
+ cachedProvider = await createProvider(config.provider.default, config.provider.model);
54
+ }
55
+ return cachedProvider;
56
+ },
57
+ async getSearchIndex() {
58
+ if (!cachedIndex) {
59
+ cachedIndex = new SearchIndex();
60
+ const loaded = await cachedIndex.load(root);
61
+ if (!loaded) {
62
+ await cachedIndex.build(root, "all");
63
+ await cachedIndex.save(root);
64
+ }
65
+ }
66
+ return cachedIndex;
67
+ },
68
+ invalidateSearch() {
69
+ cachedIndex = null;
70
+ },
71
+ };
72
+ }
73
+
74
+ // ─── Helpers ────────────────────────────────────────────────────
75
+
76
+ function ok(text: string) {
77
+ return { content: [{ type: "text" as const, text }] };
78
+ }
79
+
80
+ function err(text: string) {
81
+ return { content: [{ type: "text" as const, text }], isError: true as const };
82
+ }
83
+
84
+ function json(data: unknown) {
85
+ return ok(JSON.stringify(data, null, 2));
86
+ }
87
+
88
+ // ─── Server ─────────────────────────────────────────────────────
89
+
90
+ export async function startMcpServer(root: string) {
91
+ const ctx = createContext(root);
92
+
93
+ const server = new McpServer({
94
+ name: "kib",
95
+ version: "0.2.0",
96
+ });
97
+
98
+ // ── kib_status ────────────────────────────────────────────
99
+
100
+ server.tool(
101
+ "kib_status",
102
+ "Get vault status: source count, article count, provider, and config",
103
+ {},
104
+ async () => {
105
+ try {
106
+ const manifest = await ctx.getManifest();
107
+ const config = await ctx.getConfig();
108
+ return json({
109
+ name: manifest.vault.name,
110
+ provider: config.provider.default,
111
+ model: config.provider.model,
112
+ totalSources: manifest.stats.totalSources,
113
+ totalArticles: manifest.stats.totalArticles,
114
+ totalWords: manifest.stats.totalWords,
115
+ lastCompiled: manifest.vault.lastCompiled,
116
+ lastLint: manifest.stats.lastLintAt,
117
+ });
118
+ } catch (e) {
119
+ return err((e as Error).message);
120
+ }
121
+ },
122
+ );
123
+
124
+ // ── kib_list ──────────────────────────────────────────────
125
+
126
+ server.tool(
127
+ "kib_list",
128
+ "List all wiki articles or raw sources in the knowledge base",
129
+ {
130
+ scope: z.enum(["wiki", "raw"]).default("wiki").describe("List wiki articles or raw sources"),
131
+ },
132
+ async ({ scope }) => {
133
+ try {
134
+ const files = scope === "wiki" ? await listWiki(root) : await listRaw(root);
135
+ // Strip absolute path prefix to return vault-relative paths
136
+ const prefix = `${root}/`;
137
+ const relative = files.map((f) => f.replace(prefix, ""));
138
+ return json(relative);
139
+ } catch (e) {
140
+ return err((e as Error).message);
141
+ }
142
+ },
143
+ );
144
+
145
+ // ── kib_read ──────────────────────────────────────────────
146
+
147
+ server.tool(
148
+ "kib_read",
149
+ "Read a specific wiki article or raw source from the knowledge base",
150
+ {
151
+ path: z.string().describe("Relative path, e.g. 'concepts/attention.md'"),
152
+ scope: z.enum(["wiki", "raw"]).default("wiki").describe("Read from wiki/ or raw/"),
153
+ },
154
+ async ({ path, scope }) => {
155
+ try {
156
+ const content = scope === "wiki" ? await readWiki(root, path) : await readRaw(root, path);
157
+ return ok(content);
158
+ } catch (_e) {
159
+ return err(`File not found: ${path}`);
160
+ }
161
+ },
162
+ );
163
+
164
+ // ── kib_search ────────────────────────────────────────────
165
+
166
+ server.tool(
167
+ "kib_search",
168
+ "Search the knowledge base using full-text BM25 search",
169
+ {
170
+ query: z.string().describe("Search query"),
171
+ limit: z.number().int().positive().max(50).default(10).describe("Max results"),
172
+ },
173
+ async ({ query, limit }) => {
174
+ try {
175
+ const index = await ctx.getSearchIndex();
176
+ const results = index.search(query, { limit });
177
+ const prefix = `${root}/`;
178
+ return json(
179
+ results.map((r) => ({
180
+ title: r.title,
181
+ path: r.path.replace(prefix, ""),
182
+ score: r.score,
183
+ snippet: r.snippet,
184
+ })),
185
+ );
186
+ } catch (e) {
187
+ return err((e as Error).message);
188
+ }
189
+ },
190
+ );
191
+
192
+ // ── kib_query ─────────────────────────────────────────────
193
+
194
+ server.tool(
195
+ "kib_query",
196
+ "Ask a question against the knowledge base using RAG (retrieval-augmented generation)",
197
+ {
198
+ question: z.string().describe("Question to ask"),
199
+ max_articles: z
200
+ .number()
201
+ .int()
202
+ .positive()
203
+ .max(10)
204
+ .default(5)
205
+ .describe("Max articles to use as context"),
206
+ },
207
+ async ({ question, max_articles }) => {
208
+ try {
209
+ const provider = await ctx.getProvider();
210
+ const result = await queryVault(root, question, provider, {
211
+ maxArticles: max_articles,
212
+ });
213
+ return ok(`${result.answer}\n\n---\nSources: ${result.sourcePaths.join(", ") || "none"}`);
214
+ } catch (e) {
215
+ return err((e as Error).message);
216
+ }
217
+ },
218
+ );
219
+
220
+ // ── kib_ingest ────────────────────────────────────────────
221
+
222
+ server.tool(
223
+ "kib_ingest",
224
+ "Ingest a source (URL or file path) into the knowledge base",
225
+ {
226
+ source: z.string().describe("URL or file path to ingest"),
227
+ category: z
228
+ .string()
229
+ .optional()
230
+ .describe("Raw subdirectory override (e.g. 'papers', 'articles')"),
231
+ tags: z.string().optional().describe("Comma-separated tags"),
232
+ },
233
+ async ({ source, category, tags }) => {
234
+ try {
235
+ const result = await ingestSource(root, source, {
236
+ category,
237
+ tags: tags?.split(",").map((t) => t.trim()),
238
+ });
239
+ ctx.invalidateSearch();
240
+ return json({
241
+ path: result.path,
242
+ title: result.metadata?.title,
243
+ wordCount: result.metadata?.wordCount,
244
+ skipped: result.skipped,
245
+ skipReason: result.skipReason,
246
+ });
247
+ } catch (e) {
248
+ return err((e as Error).message);
249
+ }
250
+ },
251
+ );
252
+
253
+ // ── kib_compile ───────────────────────────────────────────
254
+
255
+ server.tool(
256
+ "kib_compile",
257
+ "Compile pending raw sources into wiki articles using the configured LLM",
258
+ {
259
+ force: z.boolean().default(false).describe("Recompile all sources"),
260
+ source: z.string().optional().describe("Compile only a specific source"),
261
+ dry_run: z.boolean().default(false).describe("Preview without writing"),
262
+ },
263
+ async ({ force, source, dry_run }) => {
264
+ try {
265
+ const provider = await ctx.getProvider();
266
+ const config = await ctx.getConfig();
267
+ const result = await compileVault(root, provider, config, {
268
+ force,
269
+ dryRun: dry_run,
270
+ sourceFilter: source,
271
+ });
272
+ ctx.invalidateSearch();
273
+ return json({
274
+ sourcesCompiled: result.sourcesCompiled,
275
+ articlesCreated: result.articlesCreated,
276
+ articlesUpdated: result.articlesUpdated,
277
+ articlesDeleted: result.articlesDeleted,
278
+ operations: result.operations,
279
+ });
280
+ } catch (e) {
281
+ return err((e as Error).message);
282
+ }
283
+ },
284
+ );
285
+
286
+ // ── kib_lint ──────────────────────────────────────────────
287
+
288
+ server.tool(
289
+ "kib_lint",
290
+ "Run health checks on the wiki and report issues",
291
+ {
292
+ rule: z
293
+ .string()
294
+ .optional()
295
+ .describe("Run only a specific rule: orphan, stale, missing, broken-link, frontmatter"),
296
+ },
297
+ async ({ rule }) => {
298
+ try {
299
+ const result = await lintVault(root, { ruleFilter: rule });
300
+ return json({
301
+ errors: result.errors,
302
+ warnings: result.warnings,
303
+ infos: result.infos,
304
+ diagnostics: result.diagnostics,
305
+ });
306
+ } catch (e) {
307
+ return err((e as Error).message);
308
+ }
309
+ },
310
+ );
311
+
312
+ // ── Resources ─────────────────────────────────────────────
313
+
314
+ server.resource("wiki-index", "wiki://index", { mimeType: "text/markdown" }, async () => {
315
+ const content = await readIndex(root);
316
+ return {
317
+ contents: [{ uri: "wiki://index", text: content || "(no index yet — run kib compile)" }],
318
+ };
319
+ });
320
+
321
+ server.resource("wiki-graph", "wiki://graph", { mimeType: "text/markdown" }, async () => {
322
+ const content = await readGraph(root);
323
+ return {
324
+ contents: [{ uri: "wiki://graph", text: content || "(no graph yet — run kib compile)" }],
325
+ };
326
+ });
327
+
328
+ // ── Start ─────────────────────────────────────────────────
329
+
330
+ const transport = new StdioServerTransport();
331
+ await server.connect(transport);
332
+ }
@@ -148,7 +148,7 @@ async function saveCredential(key: string, value: string): Promise<void> {
148
148
  const updated = lines.filter((l) => !l.startsWith(`${key}=`));
149
149
  updated.push(`${key}=${value}`);
150
150
 
151
- await writeFile(CREDENTIALS_FILE, updated.join("\n") + "\n", { mode: 0o600 });
151
+ await writeFile(CREDENTIALS_FILE, `${updated.join("\n")}\n`, { mode: 0o600 });
152
152
  }
153
153
 
154
154
  /**