@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 +29 -2
- package/package.json +3 -2
- package/src/commands/chat.ts +2 -2
- package/src/commands/compile.ts +2 -1
- package/src/commands/config.ts +13 -9
- package/src/commands/init.ts +1 -1
- package/src/commands/query.ts +2 -1
- package/src/commands/serve.ts +31 -0
- package/src/commands/skill.ts +2 -1
- package/src/commands/watch.ts +2 -2
- package/src/index.ts +9 -0
- package/src/mcp/server.ts +332 -0
- package/src/ui/setup-provider.ts +1 -1
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
|
-
#
|
|
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.
|
|
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
|
-
"
|
|
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": {
|
package/src/commands/chat.ts
CHANGED
|
@@ -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) {
|
package/src/commands/compile.ts
CHANGED
|
@@ -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 {
|
package/src/commands/config.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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 (
|
|
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:
|
|
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)) {
|
package/src/commands/init.ts
CHANGED
package/src/commands/query.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/skill.ts
CHANGED
|
@@ -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";
|
package/src/commands/watch.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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
|
+
}
|
package/src/ui/setup-provider.ts
CHANGED
|
@@ -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")
|
|
151
|
+
await writeFile(CREDENTIALS_FILE, `${updated.join("\n")}\n`, { mode: 0o600 });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|