kibi-mcp 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.
- package/bin/kibi-mcp +59 -0
- package/dist/env.js +99 -0
- package/dist/mcpcat.js +129 -0
- package/dist/server.js +673 -0
- package/dist/tools/branch.js +208 -0
- package/dist/tools/check.js +349 -0
- package/dist/tools/context.js +280 -0
- package/dist/tools/coverage-report.js +91 -0
- package/dist/tools/delete.js +100 -0
- package/dist/tools/derive.js +311 -0
- package/dist/tools/impact.js +70 -0
- package/dist/tools/list-types.js +75 -0
- package/dist/tools/prolog-list.js +176 -0
- package/dist/tools/query-relationships.js +176 -0
- package/dist/tools/query.js +364 -0
- package/dist/tools/suggest-shared-facts.js +138 -0
- package/dist/tools/symbols.js +219 -0
- package/dist/tools/upsert.js +228 -0
- package/dist/tools-config.js +448 -0
- package/dist/workspace.js +126 -0
- package/package.json +43 -0
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
const RULES = [
|
|
20
|
+
"transitively_implements",
|
|
21
|
+
"transitively_depends",
|
|
22
|
+
"impacted_by_change",
|
|
23
|
+
"affected_symbols",
|
|
24
|
+
"coverage_gap",
|
|
25
|
+
"untested_symbols",
|
|
26
|
+
"stale",
|
|
27
|
+
"orphaned",
|
|
28
|
+
"conflicting",
|
|
29
|
+
"deprecated_still_used",
|
|
30
|
+
"current_adr",
|
|
31
|
+
"adr_chain",
|
|
32
|
+
"superseded_by",
|
|
33
|
+
"domain_contradictions",
|
|
34
|
+
];
|
|
35
|
+
export async function handleKbDerive(prolog, args) {
|
|
36
|
+
const params = args.params ?? {};
|
|
37
|
+
const { rule } = args;
|
|
38
|
+
if (!RULES.includes(rule)) {
|
|
39
|
+
throw new Error(`Unsupported rule '${rule}'`);
|
|
40
|
+
}
|
|
41
|
+
let rows = [];
|
|
42
|
+
switch (rule) {
|
|
43
|
+
case "transitively_implements":
|
|
44
|
+
rows = await deriveTransitivelyImplements(prolog, params);
|
|
45
|
+
break;
|
|
46
|
+
case "transitively_depends":
|
|
47
|
+
rows = await deriveTransitivelyDepends(prolog, params);
|
|
48
|
+
break;
|
|
49
|
+
case "impacted_by_change":
|
|
50
|
+
rows = await deriveImpactedByChange(prolog, params);
|
|
51
|
+
break;
|
|
52
|
+
case "affected_symbols":
|
|
53
|
+
rows = await deriveAffectedSymbols(prolog, params);
|
|
54
|
+
break;
|
|
55
|
+
case "coverage_gap":
|
|
56
|
+
rows = await deriveCoverageGap(prolog, params);
|
|
57
|
+
break;
|
|
58
|
+
case "untested_symbols":
|
|
59
|
+
rows = await deriveUntestedSymbols(prolog);
|
|
60
|
+
break;
|
|
61
|
+
case "stale":
|
|
62
|
+
rows = await deriveStale(prolog, params);
|
|
63
|
+
break;
|
|
64
|
+
case "orphaned":
|
|
65
|
+
rows = await deriveOrphaned(prolog, params);
|
|
66
|
+
break;
|
|
67
|
+
case "conflicting":
|
|
68
|
+
rows = await deriveConflicting(prolog, params);
|
|
69
|
+
break;
|
|
70
|
+
case "deprecated_still_used":
|
|
71
|
+
rows = await deriveDeprecatedStillUsed(prolog, params);
|
|
72
|
+
break;
|
|
73
|
+
case "current_adr":
|
|
74
|
+
rows = await deriveCurrentAdr(prolog);
|
|
75
|
+
break;
|
|
76
|
+
case "adr_chain":
|
|
77
|
+
rows = await deriveAdrChain(prolog, params);
|
|
78
|
+
break;
|
|
79
|
+
case "superseded_by":
|
|
80
|
+
rows = await deriveSupersededBy(prolog, params);
|
|
81
|
+
break;
|
|
82
|
+
case "domain_contradictions":
|
|
83
|
+
rows = await deriveDomainContradictions(prolog);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: `Derived ${rows.length} row(s) for rule '${rule}'.`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
structuredContent: {
|
|
94
|
+
rule,
|
|
95
|
+
params,
|
|
96
|
+
count: rows.length,
|
|
97
|
+
rows,
|
|
98
|
+
provenance: {
|
|
99
|
+
predicate: rule,
|
|
100
|
+
deterministic: true,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function deriveTransitivelyImplements(prolog, params) {
|
|
106
|
+
const symbolFilter = asOptionalString(params.symbol);
|
|
107
|
+
const reqFilter = asOptionalString(params.req);
|
|
108
|
+
const cond = makeConjunction([
|
|
109
|
+
symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "",
|
|
110
|
+
reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "",
|
|
111
|
+
]);
|
|
112
|
+
const goal = `setof([Symbol,Req], (transitively_implements(Symbol, Req)${cond}), Rows)`;
|
|
113
|
+
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
114
|
+
return pairs.map(([symbol, req]) => ({ symbol, req }));
|
|
115
|
+
}
|
|
116
|
+
async function deriveTransitivelyDepends(prolog, params) {
|
|
117
|
+
const req1Filter = asOptionalString(params.req1);
|
|
118
|
+
const req2Filter = asOptionalString(params.req2);
|
|
119
|
+
const cond = makeConjunction([
|
|
120
|
+
req1Filter ? `Req1='${escapeAtom(req1Filter)}'` : "",
|
|
121
|
+
req2Filter ? `Req2='${escapeAtom(req2Filter)}'` : "",
|
|
122
|
+
]);
|
|
123
|
+
const goal = `setof([Req1,Req2], (transitively_depends(Req1, Req2)${cond}), Rows)`;
|
|
124
|
+
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
125
|
+
return pairs.map(([req1, req2]) => ({ req1, req2 }));
|
|
126
|
+
}
|
|
127
|
+
async function deriveImpactedByChange(prolog, params) {
|
|
128
|
+
const changed = asRequiredString(params.changed, "params.changed is required");
|
|
129
|
+
const goal = `setof(Entity, impacted_by_change(Entity, '${escapeAtom(changed)}'), Rows)`;
|
|
130
|
+
const entities = await queryAtomRows(prolog, goal, "Rows");
|
|
131
|
+
return entities.map((entity) => ({ changed, entity }));
|
|
132
|
+
}
|
|
133
|
+
async function deriveAffectedSymbols(prolog, params) {
|
|
134
|
+
const req = asRequiredString(params.req, "params.req is required");
|
|
135
|
+
const goal = `affected_symbols('${escapeAtom(req)}', Symbols)`;
|
|
136
|
+
const result = await prolog.query(goal);
|
|
137
|
+
if (!result.success || !result.bindings.Symbols) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const symbols = parseAtomList(result.bindings.Symbols);
|
|
141
|
+
return symbols.map((symbol) => ({ req, symbol }));
|
|
142
|
+
}
|
|
143
|
+
async function deriveCoverageGap(prolog, params) {
|
|
144
|
+
const reqFilter = asOptionalString(params.req);
|
|
145
|
+
const cond = reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "";
|
|
146
|
+
const goal = `setof([Req,Reason], (coverage_gap(Req, Reason)${makeConjunction([cond])}), Rows)`;
|
|
147
|
+
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
148
|
+
return pairs.map(([req, reason]) => ({ req, reason }));
|
|
149
|
+
}
|
|
150
|
+
async function deriveUntestedSymbols(prolog) {
|
|
151
|
+
const result = await prolog.query("untested_symbols(Symbols)");
|
|
152
|
+
if (!result.success || !result.bindings.Symbols) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const symbols = parseAtomList(result.bindings.Symbols);
|
|
156
|
+
return symbols.map((symbol) => ({ symbol }));
|
|
157
|
+
}
|
|
158
|
+
async function deriveStale(prolog, params) {
|
|
159
|
+
const maxAgeDays = Number(params.max_age_days ?? params.maxAgeDays);
|
|
160
|
+
if (!Number.isFinite(maxAgeDays)) {
|
|
161
|
+
throw new Error("params.max_age_days is required and must be numeric");
|
|
162
|
+
}
|
|
163
|
+
const entityFilter = asOptionalString(params.entity);
|
|
164
|
+
const cond = entityFilter ? `Entity='${escapeAtom(entityFilter)}'` : "";
|
|
165
|
+
const goal = `setof(Entity, (stale(Entity, ${maxAgeDays})${makeConjunction([cond])}), Rows)`;
|
|
166
|
+
const entities = await queryAtomRows(prolog, goal, "Rows");
|
|
167
|
+
return entities.map((entity) => ({ entity, max_age_days: maxAgeDays }));
|
|
168
|
+
}
|
|
169
|
+
async function deriveOrphaned(prolog, params) {
|
|
170
|
+
const symbolFilter = asOptionalString(params.symbol);
|
|
171
|
+
const cond = symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "";
|
|
172
|
+
const goal = `setof(Symbol, (orphaned(Symbol)${makeConjunction([cond])}), Rows)`;
|
|
173
|
+
const symbols = await queryAtomRows(prolog, goal, "Rows");
|
|
174
|
+
return symbols.map((symbol) => ({ symbol }));
|
|
175
|
+
}
|
|
176
|
+
async function deriveConflicting(prolog, params) {
|
|
177
|
+
const adr1Filter = asOptionalString(params.adr1);
|
|
178
|
+
const adr2Filter = asOptionalString(params.adr2);
|
|
179
|
+
const cond = makeConjunction([
|
|
180
|
+
adr1Filter ? `Adr1='${escapeAtom(adr1Filter)}'` : "",
|
|
181
|
+
adr2Filter ? `Adr2='${escapeAtom(adr2Filter)}'` : "",
|
|
182
|
+
]);
|
|
183
|
+
const goal = `setof([Adr1,Adr2], (conflicting(Adr1, Adr2)${cond}), Rows)`;
|
|
184
|
+
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
185
|
+
return pairs.map(([adr1, adr2]) => ({ adr1, adr2 }));
|
|
186
|
+
}
|
|
187
|
+
async function deriveDeprecatedStillUsed(prolog, params) {
|
|
188
|
+
const adrFilter = asOptionalString(params.adr);
|
|
189
|
+
const goal = adrFilter
|
|
190
|
+
? `deprecated_still_used('${escapeAtom(adrFilter)}', Symbols)`
|
|
191
|
+
: "setof([Adr,Symbols], deprecated_still_used(Adr, Symbols), Rows)";
|
|
192
|
+
if (adrFilter) {
|
|
193
|
+
const result = await prolog.query(goal);
|
|
194
|
+
if (!result.success || !result.bindings.Symbols) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
return [
|
|
198
|
+
{ adr: adrFilter, symbols: parseAtomList(result.bindings.Symbols) },
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
202
|
+
return pairs.map(([adr, symbolsRaw]) => ({
|
|
203
|
+
adr,
|
|
204
|
+
symbols: parseAtomList(symbolsRaw),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
async function queryAtomRows(prolog, goal, bindingName) {
|
|
208
|
+
const result = await prolog.query(goal);
|
|
209
|
+
if (!result.success || !result.bindings[bindingName]) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
return parseAtomList(result.bindings[bindingName]);
|
|
213
|
+
}
|
|
214
|
+
async function queryPairRows(prolog, goal, bindingName) {
|
|
215
|
+
const result = await prolog.query(goal);
|
|
216
|
+
if (!result.success || !result.bindings[bindingName]) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return parsePairList(result.bindings[bindingName]);
|
|
220
|
+
}
|
|
221
|
+
function asOptionalString(value) {
|
|
222
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
223
|
+
}
|
|
224
|
+
function asRequiredString(value, message) {
|
|
225
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
226
|
+
throw new Error(message);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function makeConjunction(parts) {
|
|
231
|
+
const filtered = parts.filter((part) => part.length > 0);
|
|
232
|
+
if (filtered.length === 0) {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
return `, ${filtered.join(", ")}`;
|
|
236
|
+
}
|
|
237
|
+
function escapeAtom(value) {
|
|
238
|
+
return value.replace(/'/g, "\\'");
|
|
239
|
+
}
|
|
240
|
+
async function deriveCurrentAdr(prolog) {
|
|
241
|
+
// Query for all current ADRs and their titles
|
|
242
|
+
const result = await prolog.query("setof([Id,TitleAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), current_adr(Id)), Rows)");
|
|
243
|
+
if (!result.success || !result.bindings.Rows) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
const pairs = parsePairList(result.bindings.Rows);
|
|
247
|
+
return pairs.map(([id, title]) => ({ id, title }));
|
|
248
|
+
}
|
|
249
|
+
async function deriveAdrChain(prolog, params) {
|
|
250
|
+
const adr = asRequiredString(params.adr, "params.adr is required");
|
|
251
|
+
// Query for the full chain including status
|
|
252
|
+
const result = await prolog.query(`findall([Id,TitleAtom,StatusAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), memberchk(status=Status, Props), normalize_term_atom(Status, StatusAtom), adr_chain('${escapeAtom(adr)}', Chain), member(Id, Chain)), Rows)`);
|
|
253
|
+
if (!result.success || !result.bindings.Rows) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
// Parse triplets and include status
|
|
257
|
+
const triplets = parseTripleList(result.bindings.Rows);
|
|
258
|
+
return triplets.map(([id, title, status]) => ({ id, title, status }));
|
|
259
|
+
}
|
|
260
|
+
async function deriveSupersededBy(prolog, params) {
|
|
261
|
+
const adr = asRequiredString(params.adr, "params.adr is required");
|
|
262
|
+
// Query for direct supersession
|
|
263
|
+
const result = await prolog.query(`superseded_by('${escapeAtom(adr)}', NewAdr), kb_entity(NewAdr, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom)`);
|
|
264
|
+
if (!result.success ||
|
|
265
|
+
!result.bindings.NewAdr ||
|
|
266
|
+
!result.bindings.TitleAtom) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const newAdr = String(result.bindings.NewAdr).replace(/^'|'$/g, "");
|
|
270
|
+
const newAdrTitle = String(result.bindings.TitleAtom).replace(/^'|'$/g, "");
|
|
271
|
+
return [
|
|
272
|
+
{
|
|
273
|
+
adr,
|
|
274
|
+
successor_id: newAdr,
|
|
275
|
+
successor_title: newAdrTitle,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
function parseTripleList(raw) {
|
|
280
|
+
const match = raw.match(/\[(.*)\]/);
|
|
281
|
+
if (!match) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
const content = match[1].trim();
|
|
285
|
+
if (!content) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
// Parse triplets: [[a,b,c],[x,y,z],...]
|
|
289
|
+
const triplets = [];
|
|
290
|
+
const tripletRegex = /\[([^,]+),([^,]+),([^\]]+)\]/g;
|
|
291
|
+
let tripletMatch;
|
|
292
|
+
do {
|
|
293
|
+
tripletMatch = tripletRegex.exec(content);
|
|
294
|
+
if (tripletMatch !== null) {
|
|
295
|
+
triplets.push([
|
|
296
|
+
tripletMatch[1].trim().replace(/^'|'$/g, ""),
|
|
297
|
+
tripletMatch[2].trim().replace(/^'|'$/g, ""),
|
|
298
|
+
tripletMatch[3].trim().replace(/^'|'$/g, ""),
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
} while (tripletMatch !== null);
|
|
302
|
+
return triplets;
|
|
303
|
+
}
|
|
304
|
+
async function deriveDomainContradictions(prolog) {
|
|
305
|
+
const result = await prolog.query("setof([ReqA,ReqB,Reason], contradicting_reqs(ReqA, ReqB, Reason), Rows)");
|
|
306
|
+
if (!result.success || !result.bindings.Rows) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const rows = parseTripleList(result.bindings.Rows);
|
|
310
|
+
return rows.map(([reqA, reqB, reason]) => ({ reqA, reqB, reason }));
|
|
311
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 } from "./prolog-list.js";
|
|
19
|
+
export async function handleKbImpact(prolog, args) {
|
|
20
|
+
if (!args.entity || typeof args.entity !== "string") {
|
|
21
|
+
throw new Error("'entity' is required");
|
|
22
|
+
}
|
|
23
|
+
const goal = `setof(Id, (impacted_by_change(Id, '${escapeAtom(args.entity)}'), Id \\= '${escapeAtom(args.entity)}'), Impacted)`;
|
|
24
|
+
const impactedIds = await queryAtoms(prolog, goal, "Impacted");
|
|
25
|
+
const impacted = [];
|
|
26
|
+
for (const id of impactedIds) {
|
|
27
|
+
const type = await getEntityType(prolog, id);
|
|
28
|
+
impacted.push({ id, type: type ?? "unknown" });
|
|
29
|
+
}
|
|
30
|
+
impacted.sort((a, b) => {
|
|
31
|
+
if (a.type === b.type) {
|
|
32
|
+
return a.id.localeCompare(b.id);
|
|
33
|
+
}
|
|
34
|
+
return a.type.localeCompare(b.type);
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Impact analysis for '${args.entity}': ${impacted.length} impacted entity(s).`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
structuredContent: {
|
|
44
|
+
entity: args.entity,
|
|
45
|
+
impacted,
|
|
46
|
+
count: impacted.length,
|
|
47
|
+
provenance: {
|
|
48
|
+
predicate: "impacted_by_change",
|
|
49
|
+
deterministic: true,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function queryAtoms(prolog, goal, bindingName) {
|
|
55
|
+
const result = await prolog.query(goal);
|
|
56
|
+
if (!result.success || !result.bindings[bindingName]) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return parseAtomList(result.bindings[bindingName]);
|
|
60
|
+
}
|
|
61
|
+
async function getEntityType(prolog, id) {
|
|
62
|
+
const result = await prolog.query(`kb_entity('${escapeAtom(id)}', Type, _)`);
|
|
63
|
+
if (!result.success || !result.bindings.Type) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return result.bindings.Type;
|
|
67
|
+
}
|
|
68
|
+
function escapeAtom(value) {
|
|
69
|
+
return value.replace(/'/g, "\\'");
|
|
70
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
* Handle kb_list_entity_types tool calls
|
|
20
|
+
* Returns the static list of supported KB entity type names (req, scenario, test, adr, flag, event, symbol, fact).
|
|
21
|
+
*/
|
|
22
|
+
export async function handleKbListEntityTypes() {
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: "Available entity types: req, scenario, test, adr, flag, event, symbol, fact",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
structuredContent: {
|
|
31
|
+
types: [
|
|
32
|
+
"req",
|
|
33
|
+
"scenario",
|
|
34
|
+
"test",
|
|
35
|
+
"adr",
|
|
36
|
+
"flag",
|
|
37
|
+
"event",
|
|
38
|
+
"symbol",
|
|
39
|
+
"fact",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Handle kb_list_relationship_types tool calls
|
|
46
|
+
* Returns the static list of supported KB relationship type names (depends_on, specified_by, verified_by, etc.).
|
|
47
|
+
*/
|
|
48
|
+
export async function handleKbListRelationshipTypes() {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: "Available relationship types: depends_on, specified_by, verified_by, validates, implements, covered_by, constrained_by, constrains, requires_property, guards, publishes, consumes, supersedes, relates_to",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
structuredContent: {
|
|
57
|
+
types: [
|
|
58
|
+
"depends_on",
|
|
59
|
+
"specified_by",
|
|
60
|
+
"verified_by",
|
|
61
|
+
"validates",
|
|
62
|
+
"implements",
|
|
63
|
+
"covered_by",
|
|
64
|
+
"constrained_by",
|
|
65
|
+
"constrains",
|
|
66
|
+
"requires_property",
|
|
67
|
+
"guards",
|
|
68
|
+
"publishes",
|
|
69
|
+
"consumes",
|
|
70
|
+
"supersedes",
|
|
71
|
+
"relates_to",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
export function parseAtomList(raw) {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
if (trimmed === "[]" || trimmed.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const content = unwrapList(trimmed);
|
|
51
|
+
if (content.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return splitTopLevel(content, ",")
|
|
55
|
+
.map((token) => stripQuotes(token.trim()))
|
|
56
|
+
.filter((token) => token.length > 0);
|
|
57
|
+
}
|
|
58
|
+
export function parsePairList(raw) {
|
|
59
|
+
const rows = parseListRows(raw);
|
|
60
|
+
const pairs = [];
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
|
|
63
|
+
if (parts.length >= 2) {
|
|
64
|
+
pairs.push([parts[0], parts[1]]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return pairs;
|
|
68
|
+
}
|
|
69
|
+
export function parseTriples(raw) {
|
|
70
|
+
const rows = parseListRows(raw);
|
|
71
|
+
const triples = [];
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
|
|
74
|
+
if (parts.length >= 3) {
|
|
75
|
+
triples.push([parts[0], parts[1], parts[2]]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return triples;
|
|
79
|
+
}
|
|
80
|
+
function parseListRows(raw) {
|
|
81
|
+
const trimmed = raw.trim();
|
|
82
|
+
if (trimmed === "[]" || trimmed.length === 0) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const content = unwrapList(trimmed);
|
|
86
|
+
if (content.length === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const rows = [];
|
|
90
|
+
let depth = 0;
|
|
91
|
+
let current = "";
|
|
92
|
+
for (let i = 0; i < content.length; i++) {
|
|
93
|
+
const ch = content[i];
|
|
94
|
+
if (ch === "[") {
|
|
95
|
+
depth++;
|
|
96
|
+
if (depth > 1) {
|
|
97
|
+
current += ch;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === "]") {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0) {
|
|
104
|
+
rows.push(current.trim());
|
|
105
|
+
current = "";
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
current += ch;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === "," && depth === 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
current += ch;
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
function unwrapList(value) {
|
|
120
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
121
|
+
return value.slice(1, -1).trim();
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function splitTopLevel(input, delimiter) {
|
|
126
|
+
const parts = [];
|
|
127
|
+
let current = "";
|
|
128
|
+
let depth = 0;
|
|
129
|
+
let inDoubleQuotes = false;
|
|
130
|
+
let inSingleQuotes = false;
|
|
131
|
+
for (let i = 0; i < input.length; i++) {
|
|
132
|
+
const ch = input[i];
|
|
133
|
+
const prev = i > 0 ? input[i - 1] : "";
|
|
134
|
+
if (ch === '"' && !inSingleQuotes && prev !== "\\") {
|
|
135
|
+
inDoubleQuotes = !inDoubleQuotes;
|
|
136
|
+
current += ch;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === "'" && !inDoubleQuotes && prev !== "\\") {
|
|
140
|
+
inSingleQuotes = !inSingleQuotes;
|
|
141
|
+
current += ch;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!inSingleQuotes && !inDoubleQuotes && (ch === "[" || ch === "(")) {
|
|
145
|
+
depth++;
|
|
146
|
+
current += ch;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!inSingleQuotes && !inDoubleQuotes && (ch === "]" || ch === ")")) {
|
|
150
|
+
depth--;
|
|
151
|
+
current += ch;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!inSingleQuotes && !inDoubleQuotes && depth === 0 && ch === delimiter) {
|
|
155
|
+
if (current.length > 0) {
|
|
156
|
+
parts.push(current);
|
|
157
|
+
}
|
|
158
|
+
current = "";
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
current += ch;
|
|
162
|
+
}
|
|
163
|
+
if (current.length > 0) {
|
|
164
|
+
parts.push(current);
|
|
165
|
+
}
|
|
166
|
+
return parts;
|
|
167
|
+
}
|
|
168
|
+
function stripQuotes(value) {
|
|
169
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
170
|
+
return value.slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
173
|
+
return value.slice(1, -1);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|