kibi-mcp 0.2.4 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +9,8 @@
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-cli": "^0.2.3",
13
- "kibi-core": "^0.1.8",
12
+ "kibi-cli": "^0.2.6",
13
+ "kibi-core": "^0.1.10",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -1,208 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- /*
19
- How to apply this header to source files (examples)
20
-
21
- 1) Prepend header to a single file (POSIX shells):
22
-
23
- cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
24
-
25
- 2) Apply to multiple files (example: the project's main entry files):
26
-
27
- for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
28
- if [ -f "$f" ]; then
29
- cp "$f" "$f".bak
30
- (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
31
- fi
32
- done
33
-
34
- 3) Avoid duplicating the header: run a quick guard to only add if missing
35
-
36
- for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
37
- if [ -f "$f" ]; then
38
- if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
39
- cp "$f" "$f".bak
40
- (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
41
- fi
42
- fi
43
- done
44
- */
45
- import { execSync } from "node:child_process";
46
- import * as fs from "node:fs";
47
- import * as path from "node:path";
48
- import { resolveKbPath, resolveWorkspaceRoot } from "../workspace.js";
49
- /**
50
- * Handle kb_branch_ensure tool calls - create branch KB if not exists
51
- */
52
- export async function handleKbBranchEnsure(_prolog, args) {
53
- const { branch } = args;
54
- if (!branch || branch.trim() === "") {
55
- throw new Error("Branch name is required");
56
- }
57
- // Sanitize branch name (prevent path traversal)
58
- const isSafe = (name) => {
59
- // No empty or excessively long names
60
- if (!name || name.length > 255)
61
- return false;
62
- // No path traversal or absolute paths
63
- if (name.includes("..") || path.isAbsolute(name) || name.startsWith("/")) {
64
- return false;
65
- }
66
- // Whitelist characters (alphanumeric, dot, underscore, hyphen, forward slash)
67
- if (!/^[a-zA-Z0-9._\-/]+$/.test(name))
68
- return false;
69
- // No redundant slashes or trailing slash/dot
70
- if (name.includes("//") ||
71
- name.endsWith("/") ||
72
- name.endsWith(".") ||
73
- name.includes("\\")) {
74
- return false;
75
- }
76
- return true;
77
- };
78
- if (!isSafe(branch)) {
79
- throw new Error(`Invalid branch name: ${branch}`);
80
- }
81
- const safeBranch = branch;
82
- try {
83
- const workspaceRoot = resolveWorkspaceRoot();
84
- const branchPath = resolveKbPath(workspaceRoot, safeBranch);
85
- const developPath = resolveKbPath(workspaceRoot, "develop");
86
- // Check if branch KB already exists
87
- if (fs.existsSync(branchPath)) {
88
- return {
89
- content: [
90
- {
91
- type: "text",
92
- text: `Branch KB '${safeBranch}' already exists`,
93
- },
94
- ],
95
- structuredContent: {
96
- created: false,
97
- path: branchPath,
98
- },
99
- };
100
- }
101
- // Ensure develop branch exists
102
- if (!fs.existsSync(developPath)) {
103
- throw new Error("Develop branch KB does not exist. Run 'kb init' first.");
104
- }
105
- // Copy develop branch KB to new branch
106
- fs.cpSync(developPath, branchPath, { recursive: true });
107
- return {
108
- content: [
109
- {
110
- type: "text",
111
- text: `Created branch KB '${safeBranch}' from develop`,
112
- },
113
- ],
114
- structuredContent: {
115
- created: true,
116
- path: branchPath,
117
- },
118
- };
119
- }
120
- catch (error) {
121
- const message = error instanceof Error ? error.message : String(error);
122
- throw new Error(`Branch ensure failed: ${message}`);
123
- }
124
- }
125
- /**
126
- * Handle kb_branch_gc tool calls - garbage collect stale branch KBs
127
- */
128
- export async function handleKbBranchGc(_prolog, args) {
129
- const { dry_run = true } = args;
130
- try {
131
- const workspaceRoot = resolveWorkspaceRoot();
132
- const kbRoot = path.dirname(resolveKbPath(workspaceRoot, "develop"));
133
- // Check if .kb/branches exists
134
- if (!fs.existsSync(kbRoot)) {
135
- return {
136
- content: [
137
- {
138
- type: "text",
139
- text: "No branch KBs found (.kb/branches does not exist)",
140
- },
141
- ],
142
- structuredContent: {
143
- stale: [],
144
- deleted: 0,
145
- },
146
- };
147
- }
148
- let gitBranches;
149
- try {
150
- execSync("git rev-parse --git-dir", {
151
- encoding: "utf-8",
152
- cwd: workspaceRoot,
153
- stdio: ["pipe", "pipe", "pipe"],
154
- env: process.env,
155
- });
156
- const output = execSync("git branch --format='%(refname:short)'", {
157
- encoding: "utf-8",
158
- cwd: workspaceRoot,
159
- stdio: ["pipe", "pipe", "pipe"],
160
- env: process.env,
161
- });
162
- gitBranches = new Set(output
163
- .trim()
164
- .split("\n")
165
- .map((b) => b.trim().replace(/^'|'$/g, ""))
166
- .filter((b) => b));
167
- }
168
- catch (error) {
169
- const message = error instanceof Error ? error.message : String(error);
170
- throw new Error(`Not in a git repository or git command failed: ${message}`);
171
- }
172
- // Get all KB branches
173
- const kbBranches = fs
174
- .readdirSync(kbRoot, { withFileTypes: true })
175
- .filter((dirent) => dirent.isDirectory())
176
- .map((dirent) => dirent.name);
177
- // Find stale branches (KB exists but git branch doesn't, excluding develop)
178
- const staleBranches = kbBranches.filter((kb) => kb !== "develop" && !gitBranches.has(kb));
179
- // Delete stale branches if not dry run
180
- let deletedCount = 0;
181
- if (!dry_run && staleBranches.length > 0) {
182
- for (const branch of staleBranches) {
183
- const branchPath = path.join(kbRoot, branch);
184
- fs.rmSync(branchPath, { recursive: true, force: true });
185
- deletedCount++;
186
- }
187
- }
188
- const summary = dry_run
189
- ? `Found ${staleBranches.length} stale branch KB(s) (dry run - not deleted)`
190
- : `Deleted ${deletedCount} stale branch KB(s)`;
191
- return {
192
- content: [
193
- {
194
- type: "text",
195
- text: summary,
196
- },
197
- ],
198
- structuredContent: {
199
- stale: staleBranches,
200
- deleted: deletedCount,
201
- },
202
- };
203
- }
204
- catch (error) {
205
- const message = error instanceof Error ? error.message : String(error);
206
- throw new Error(`Branch GC failed: ${message}`);
207
- }
208
- }
@@ -1,270 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- export async function handleKbContext(prolog, args) {
19
- const { sourceFile } = args;
20
- try {
21
- const safeSource = sourceFile.replace(/'/g, "\\'");
22
- const entityGoal = `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
23
- const entityQueryResult = await prolog.query(entityGoal);
24
- const entities = [];
25
- const entityIds = [];
26
- if (entityQueryResult.success && entityQueryResult.bindings.Results) {
27
- const entitiesData = parseListOfLists(entityQueryResult.bindings.Results);
28
- for (const data of entitiesData) {
29
- const entity = parseEntityFromList(data);
30
- entities.push({
31
- id: entity.id,
32
- type: entity.type,
33
- title: entity.title,
34
- status: entity.status,
35
- tags: entity.tags || [],
36
- });
37
- entityIds.push(entity.id);
38
- }
39
- }
40
- const relationships = [];
41
- for (const entityId of entityIds) {
42
- const relGoal = `findall([RelType,FromId,ToId], (kb_relationship(RelType, FromId, ToId), (FromId = '${entityId}' ; ToId = '${entityId}')), RelResults)`;
43
- const relQueryResult = await prolog.query(relGoal);
44
- if (relQueryResult.success && relQueryResult.bindings.RelResults) {
45
- const relData = parseListOfLists(relQueryResult.bindings.RelResults);
46
- for (const rel of relData) {
47
- relationships.push({
48
- relType: rel[0],
49
- fromId: rel[1],
50
- toId: rel[2],
51
- });
52
- }
53
- }
54
- }
55
- const text = entities.length > 0
56
- ? `Found ${entities.length} KB entities linked to source file "${sourceFile}": ${entities.map((e) => e.id).join(", ")}`
57
- : `No KB entities found for source file "${sourceFile}"`;
58
- return {
59
- content: [
60
- {
61
- type: "text",
62
- text,
63
- },
64
- ],
65
- structuredContent: {
66
- sourceFile,
67
- entities,
68
- relationships,
69
- provenance: {
70
- predicate: "kb_entities_by_source",
71
- deterministic: true,
72
- },
73
- },
74
- };
75
- }
76
- catch (error) {
77
- const message = error instanceof Error ? error.message : String(error);
78
- throw new Error(`Context query failed: ${message}`);
79
- }
80
- }
81
- function parseListOfLists(listStr) {
82
- const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
83
- if (cleaned === "") {
84
- return [];
85
- }
86
- const results = [];
87
- let depth = 0;
88
- let current = "";
89
- let currentList = [];
90
- for (let i = 0; i < cleaned.length; i++) {
91
- const char = cleaned[i];
92
- if (char === "[") {
93
- depth++;
94
- if (depth > 1)
95
- current += char;
96
- }
97
- else if (char === "]") {
98
- depth--;
99
- if (depth === 0) {
100
- if (current) {
101
- currentList.push(current.trim());
102
- current = "";
103
- }
104
- if (currentList.length > 0) {
105
- results.push(currentList);
106
- currentList = [];
107
- }
108
- }
109
- else {
110
- current += char;
111
- }
112
- }
113
- else if (char === "," && depth === 1) {
114
- if (current) {
115
- currentList.push(current.trim());
116
- current = "";
117
- }
118
- }
119
- else if (char === "," && depth === 0) {
120
- }
121
- else {
122
- current += char;
123
- }
124
- }
125
- return results;
126
- }
127
- function parseEntityFromList(data) {
128
- if (data.length < 3) {
129
- return {};
130
- }
131
- const id = data[0].trim();
132
- const type = data[1].trim();
133
- const propsStr = data[2].trim();
134
- const props = parsePropertyList(propsStr);
135
- return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
136
- }
137
- function parsePropertyList(propsStr) {
138
- const props = {};
139
- let cleaned = propsStr.trim();
140
- if (cleaned.startsWith("[")) {
141
- cleaned = cleaned.substring(1);
142
- }
143
- if (cleaned.endsWith("]")) {
144
- cleaned = cleaned.substring(0, cleaned.length - 1);
145
- }
146
- const pairs = splitTopLevel(cleaned, ",");
147
- for (const pair of pairs) {
148
- const eqIndex = pair.indexOf("=");
149
- if (eqIndex === -1)
150
- continue;
151
- const key = pair.substring(0, eqIndex).trim();
152
- const value = pair.substring(eqIndex + 1).trim();
153
- if (key === "..." || value === "..." || value === "...|...") {
154
- continue;
155
- }
156
- const parsed = parsePrologValue(value);
157
- props[key] = parsed;
158
- }
159
- return props;
160
- }
161
- function parsePrologValue(valueInput) {
162
- const value = valueInput.trim();
163
- if (value.startsWith("^^(")) {
164
- const innerStart = value.indexOf("(") + 1;
165
- let depth = 1;
166
- let innerEnd = innerStart;
167
- for (let i = innerStart; i < value.length; i++) {
168
- if (value[i] === "(")
169
- depth++;
170
- if (value[i] === ")") {
171
- depth--;
172
- if (depth === 0) {
173
- innerEnd = i;
174
- break;
175
- }
176
- }
177
- }
178
- const innerContent = value.substring(innerStart, innerEnd);
179
- const parts = splitTopLevel(innerContent, ",");
180
- if (parts.length >= 2) {
181
- let literalValue = parts[0].trim();
182
- if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
183
- literalValue = literalValue.substring(1, literalValue.length - 1);
184
- }
185
- if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
186
- const listContent = literalValue.substring(1, literalValue.length - 1);
187
- if (listContent === "") {
188
- return [];
189
- }
190
- return listContent.split(",").map((item) => item.trim());
191
- }
192
- return literalValue;
193
- }
194
- }
195
- if (value.startsWith("file:///")) {
196
- const lastSlash = value.lastIndexOf("/");
197
- if (lastSlash !== -1) {
198
- return value.substring(lastSlash + 1);
199
- }
200
- return value;
201
- }
202
- if (value.startsWith('"') && value.endsWith('"')) {
203
- return value.substring(1, value.length - 1);
204
- }
205
- if (value.startsWith("'") && value.endsWith("'")) {
206
- return value.substring(1, value.length - 1);
207
- }
208
- if (value.startsWith("[") && value.endsWith("]")) {
209
- const listContent = value.substring(1, value.length - 1);
210
- if (listContent === "") {
211
- return [];
212
- }
213
- const items = listContent.split(",").map((item) => {
214
- return parsePrologValue(item.trim());
215
- });
216
- return items;
217
- }
218
- return value;
219
- }
220
- function splitTopLevel(str, delimiter) {
221
- const results = [];
222
- let current = "";
223
- let depth = 0;
224
- let inQuotes = false;
225
- for (let i = 0; i < str.length; i++) {
226
- const char = str[i];
227
- const prevChar = i > 0 ? str[i - 1] : "";
228
- if (char === '"' && prevChar !== "\\") {
229
- inQuotes = !inQuotes;
230
- current += char;
231
- }
232
- else if (!inQuotes && (char === "[" || char === "(")) {
233
- depth++;
234
- current += char;
235
- }
236
- else if (!inQuotes && (char === "]" || char === ")")) {
237
- depth--;
238
- current += char;
239
- }
240
- else if (!inQuotes && depth === 0 && char === delimiter) {
241
- if (current) {
242
- results.push(current);
243
- current = "";
244
- }
245
- }
246
- else {
247
- current += char;
248
- }
249
- }
250
- if (current) {
251
- results.push(current);
252
- }
253
- return results;
254
- }
255
- function stripOuterQuotes(value) {
256
- if (value.startsWith("'") && value.endsWith("'")) {
257
- return value.slice(1, -1);
258
- }
259
- if (value.startsWith('"') && value.endsWith('"')) {
260
- return value.slice(1, -1);
261
- }
262
- return value;
263
- }
264
- function normalizeEntityId(value) {
265
- if (!value.startsWith("file:///")) {
266
- return value;
267
- }
268
- const idx = value.lastIndexOf("/");
269
- return idx === -1 ? value : value.slice(idx + 1);
270
- }
@@ -1,91 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- import { parseAtomList, parsePairList } from "./prolog-list.js";
19
- export async function handleKbCoverageReport(prolog, args) {
20
- const requested = args.type ?? "all";
21
- if (args.type && args.type !== "req" && args.type !== "symbol") {
22
- throw new Error("'type' must be one of: req, symbol");
23
- }
24
- const coverage = {};
25
- const predicates = [];
26
- if (requested === "all" || requested === "req") {
27
- const reqIds = await queryAtoms(prolog, "setof(Req, kb_entity(Req, req, _), Reqs)", "Reqs");
28
- const gapPairs = await queryPairs(prolog, "setof([Req,Reason], coverage_gap(Req, Reason), Rows)", "Rows");
29
- const gaps = gapPairs.map(([req, reason]) => ({ req, reason }));
30
- coverage.requirements = {
31
- total: reqIds.length,
32
- with_gaps: gaps.length,
33
- healthy: Math.max(reqIds.length - gaps.length, 0),
34
- gaps,
35
- };
36
- predicates.push("coverage_gap");
37
- }
38
- if (requested === "all" || requested === "symbol") {
39
- const symbolIds = await queryAtoms(prolog, "setof(Symbol, kb_entity(Symbol, symbol, _), Symbols)", "Symbols");
40
- const untestedResult = await prolog.query("untested_symbols(Symbols)");
41
- const untestedSymbols = untestedResult.success && untestedResult.bindings.Symbols
42
- ? parseAtomList(untestedResult.bindings.Symbols)
43
- : [];
44
- coverage.symbols = {
45
- total: symbolIds.length,
46
- untested: untestedSymbols.length,
47
- tested: Math.max(symbolIds.length - untestedSymbols.length, 0),
48
- untested_symbols: untestedSymbols,
49
- };
50
- predicates.push("untested_symbols");
51
- }
52
- const summaryParts = [];
53
- if (coverage.requirements) {
54
- summaryParts.push(`${coverage.requirements.healthy}/${coverage.requirements.total} requirements healthy`);
55
- }
56
- if (coverage.symbols) {
57
- summaryParts.push(`${coverage.symbols.tested}/${coverage.symbols.total} symbols tested`);
58
- }
59
- return {
60
- content: [
61
- {
62
- type: "text",
63
- text: summaryParts.length > 0
64
- ? `Coverage report: ${summaryParts.join("; ")}.`
65
- : "Coverage report: no data.",
66
- },
67
- ],
68
- structuredContent: {
69
- requested_type: requested,
70
- coverage,
71
- provenance: {
72
- deterministic: true,
73
- predicates,
74
- },
75
- },
76
- };
77
- }
78
- async function queryAtoms(prolog, goal, bindingName) {
79
- const result = await prolog.query(goal);
80
- if (!result.success || !result.bindings[bindingName]) {
81
- return [];
82
- }
83
- return parseAtomList(result.bindings[bindingName]);
84
- }
85
- async function queryPairs(prolog, goal, bindingName) {
86
- const result = await prolog.query(goal);
87
- if (!result.success || !result.bindings[bindingName]) {
88
- return [];
89
- }
90
- return parsePairList(result.bindings[bindingName]);
91
- }