kibi-mcp 0.7.1 → 0.9.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/dist/server/docs.js +64 -50
- package/dist/server/tools.js +12 -0
- package/dist/tools/autopilot-candidates.js +229 -0
- package/dist/tools/autopilot-discovery.js +243 -0
- package/dist/tools/autopilot-generate.js +271 -0
- package/dist/tools/briefing-generate.js +529 -0
- package/dist/tools-config.js +61 -1
- package/package.json +2 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
|
|
3
|
+
import { classifyActivationState, discoverSources as discoverActivationSources, } from "./autopilot-discovery.js";
|
|
4
|
+
import { loadEntities } from "./entity-query.js";
|
|
5
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
6
|
+
function extractTextRefFromApplyPlan(applyPlan) {
|
|
7
|
+
if (!Array.isArray(applyPlan) || applyPlan.length === 0)
|
|
8
|
+
return "";
|
|
9
|
+
const first = applyPlan[0];
|
|
10
|
+
if (!first || typeof first !== "object")
|
|
11
|
+
return "";
|
|
12
|
+
const firstRecord = first;
|
|
13
|
+
const properties = firstRecord.properties;
|
|
14
|
+
if (!properties || typeof properties !== "object")
|
|
15
|
+
return "";
|
|
16
|
+
const propsRecord = properties;
|
|
17
|
+
const textRef = propsRecord.text_ref;
|
|
18
|
+
return typeof textRef === "string" ? textRef : "";
|
|
19
|
+
}
|
|
20
|
+
function toSuppressedCandidate(reason, candidate) {
|
|
21
|
+
return {
|
|
22
|
+
candidateId: String(candidate.candidateId ?? ""),
|
|
23
|
+
reason,
|
|
24
|
+
sourcePath: String(candidate.sourcePath ?? ""),
|
|
25
|
+
entityType: String(candidate.entityType ?? ""),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function activationReasonFor(state) {
|
|
29
|
+
switch (state) {
|
|
30
|
+
case "vendored_only":
|
|
31
|
+
return "Workspace appears to contain vendored Kibi sources only; no local candidates generated.";
|
|
32
|
+
case "root_partial":
|
|
33
|
+
return "Workspace root is partially configured; discovery completed using available sources.";
|
|
34
|
+
case "root_active_seeded":
|
|
35
|
+
return "KB attached and discovery completed for a seeded workspace.";
|
|
36
|
+
case "root_active_thin":
|
|
37
|
+
return "KB attached and discovery completed for a thin workspace.";
|
|
38
|
+
default:
|
|
39
|
+
return "Workspace root is not fully initialized; discovery completed using the resolved workspace root.";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function splitDiscoveredSources(workspaceRoot, candidates) {
|
|
43
|
+
const markdownFiles = [];
|
|
44
|
+
const manifestFiles = [];
|
|
45
|
+
for (const relativePath of candidates) {
|
|
46
|
+
const absolutePath = path.resolve(workspaceRoot, relativePath);
|
|
47
|
+
if (/symbols\.ya?ml$/i.test(relativePath)) {
|
|
48
|
+
manifestFiles.push(absolutePath);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/\.md$/i.test(relativePath)) {
|
|
52
|
+
markdownFiles.push(absolutePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { markdownFiles, manifestFiles };
|
|
56
|
+
}
|
|
57
|
+
export async function handleKbAutopilotGenerate(// implements REQ-mcp-init-kibi-autopilot-v1
|
|
58
|
+
_prolog, args) {
|
|
59
|
+
const { includeGenericMarkdown = true, minConfidence = 0.8, maxCandidates = 50, entityTypes, } = args;
|
|
60
|
+
// Minimal discovery + candidate assembly implementation
|
|
61
|
+
const prolog = _prolog;
|
|
62
|
+
// Gather existing entity ids to suppress duplicates
|
|
63
|
+
let existingIds = new Set();
|
|
64
|
+
try {
|
|
65
|
+
const entities = await loadEntities(prolog, {});
|
|
66
|
+
for (const e of entities) {
|
|
67
|
+
const id = String(e.id ?? "");
|
|
68
|
+
if (id)
|
|
69
|
+
existingIds.add(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// If we can't list entities, proceed with empty set
|
|
74
|
+
existingIds = new Set();
|
|
75
|
+
}
|
|
76
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
77
|
+
const activationState = await classifyActivationState(workspaceRoot, prolog);
|
|
78
|
+
const activationDiscovery = discoverActivationSources(workspaceRoot, activationState);
|
|
79
|
+
const discovery = splitDiscoveredSources(workspaceRoot, activationDiscovery.candidates);
|
|
80
|
+
const allowGeneration = activationState === "root_uninitialized" || activationState === "root_partial";
|
|
81
|
+
if (!allowGeneration) {
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: "Autopilot generated 0 candidate(s).",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
structuredContent: {
|
|
90
|
+
activationState,
|
|
91
|
+
activationReason: activationReasonFor(activationState),
|
|
92
|
+
applyBlocked: true,
|
|
93
|
+
discoverySummary: {
|
|
94
|
+
markdownFiles: discovery.markdownFiles.length,
|
|
95
|
+
manifestFiles: discovery.manifestFiles.length,
|
|
96
|
+
vendored: activationDiscovery.summary.vendored ?? [],
|
|
97
|
+
},
|
|
98
|
+
candidates: [],
|
|
99
|
+
suppressedCandidates: [],
|
|
100
|
+
payoffSummary: {
|
|
101
|
+
current: {},
|
|
102
|
+
projectedIfAllApplied: {},
|
|
103
|
+
delta: {},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const typedMarkdownCandidates = buildTypedMarkdownCandidates(discovery, {
|
|
109
|
+
ids: existingIds,
|
|
110
|
+
workspaceRoot,
|
|
111
|
+
});
|
|
112
|
+
const manifestCandidates = buildSymbolManifestCandidates(discovery, {
|
|
113
|
+
ids: existingIds,
|
|
114
|
+
workspaceRoot,
|
|
115
|
+
});
|
|
116
|
+
// Lazy import to avoid circulars if any
|
|
117
|
+
// buildGenericMarkdownCandidates is added in autopilot-candidates
|
|
118
|
+
let genericCandidates = [];
|
|
119
|
+
if (includeGenericMarkdown) {
|
|
120
|
+
try {
|
|
121
|
+
// Import from same module file
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
123
|
+
const ac = await import("./autopilot-candidates.js");
|
|
124
|
+
if (typeof ac.buildGenericMarkdownCandidates === "function") {
|
|
125
|
+
genericCandidates = ac.buildGenericMarkdownCandidates(discovery, {
|
|
126
|
+
ids: existingIds,
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
}, minConfidence);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// ignore import failures and proceed with typed candidates only
|
|
133
|
+
genericCandidates = [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Merge and filter candidates by requested entityTypes and minConfidence
|
|
137
|
+
let allCandidates = [...typedMarkdownCandidates, ...manifestCandidates, ...genericCandidates];
|
|
138
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
139
|
+
const allowed = new Set(entityTypes);
|
|
140
|
+
allCandidates = allCandidates.filter((c) => allowed.has(c.entityType));
|
|
141
|
+
}
|
|
142
|
+
allCandidates = allCandidates.filter((c) => c.confidence >= minConfidence);
|
|
143
|
+
// Limit and deterministic sort (confidence desc, sourcePath asc)
|
|
144
|
+
allCandidates.sort((a, b) => {
|
|
145
|
+
if (b.confidence !== a.confidence)
|
|
146
|
+
return b.confidence - a.confidence;
|
|
147
|
+
if (a.sourcePath < b.sourcePath)
|
|
148
|
+
return -1;
|
|
149
|
+
if (a.sourcePath > b.sourcePath)
|
|
150
|
+
return 1;
|
|
151
|
+
return 0;
|
|
152
|
+
});
|
|
153
|
+
allCandidates = allCandidates.slice(0, maxCandidates);
|
|
154
|
+
// Dedupe logic
|
|
155
|
+
const seenByKey = new Map();
|
|
156
|
+
const suppressed = [];
|
|
157
|
+
// Helpers
|
|
158
|
+
function normalizeTitle(entityType, title) {
|
|
159
|
+
return `${entityType}::${String(title).trim().toLowerCase().replace(/\s+/g, " ")}`;
|
|
160
|
+
}
|
|
161
|
+
const typedTitleKeys = new Set(typedMarkdownCandidates.map((candidate) => normalizeTitle(String(candidate.entityType || ""), String(candidate.title || ""))));
|
|
162
|
+
for (const c of allCandidates) {
|
|
163
|
+
const record = { ...c };
|
|
164
|
+
const entityType = String(c.entityType || "");
|
|
165
|
+
const title = String(c.title || "");
|
|
166
|
+
const sourceKind = String(c.sourceKind || "");
|
|
167
|
+
const sourcePath = String(c.sourcePath || "");
|
|
168
|
+
const textRef = extractTextRefFromApplyPlan(c.applyPlan);
|
|
169
|
+
const titleKey = normalizeTitle(entityType, title);
|
|
170
|
+
// entity_exists: exact entity ID present in KB
|
|
171
|
+
const upsert = Array.isArray(c.applyPlan) ? c.applyPlan[0] : null;
|
|
172
|
+
let upsertId = "";
|
|
173
|
+
if (upsert && typeof upsert === "object") {
|
|
174
|
+
const upsertRecord = upsert;
|
|
175
|
+
const directId = upsertRecord.id;
|
|
176
|
+
if (typeof directId === "string" && directId.length > 0) {
|
|
177
|
+
upsertId = directId;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const properties = upsertRecord.properties;
|
|
181
|
+
if (properties && typeof properties === "object") {
|
|
182
|
+
const nestedId = properties.id;
|
|
183
|
+
if (typeof nestedId === "string" && nestedId.length > 0) {
|
|
184
|
+
upsertId = nestedId;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (existingIds.has(upsertId)) {
|
|
190
|
+
suppressed.push(toSuppressedCandidate("entity_exists", record));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (sourceKind === "generic_markdown" && typedTitleKeys.has(titleKey)) {
|
|
194
|
+
suppressed.push(toSuppressedCandidate("shadowed_by_typed_source", record));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// duplicate_title: same entityType + normalized title
|
|
198
|
+
const existing = seenByKey.get(titleKey);
|
|
199
|
+
if (existing) {
|
|
200
|
+
// keep the higher confidence one
|
|
201
|
+
const existingConf = Number(existing.confidence ?? 0);
|
|
202
|
+
const thisConf = Number(c.confidence ?? 0);
|
|
203
|
+
if (thisConf > existingConf) {
|
|
204
|
+
// move existing to suppressed
|
|
205
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", existing));
|
|
206
|
+
seenByKey.set(titleKey, record);
|
|
207
|
+
}
|
|
208
|
+
else if (thisConf < existingConf) {
|
|
209
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", record));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// tie-break by lexicographically smallest sourcePath:textRef
|
|
213
|
+
const existingRef = `${String(existing.sourcePath ?? "")}::${extractTextRefFromApplyPlan(existing.applyPlan)}`;
|
|
214
|
+
const thisRef = `${sourcePath}::${textRef}`;
|
|
215
|
+
if (thisRef < existingRef) {
|
|
216
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", existing));
|
|
217
|
+
seenByKey.set(titleKey, record);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", record));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
seenByKey.set(titleKey, record);
|
|
226
|
+
}
|
|
227
|
+
const candidateRecords = Array.from(seenByKey.values());
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: `Autopilot generated ${allCandidates.length} candidate(s).`,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
structuredContent: {
|
|
236
|
+
activationState,
|
|
237
|
+
activationReason: activationReasonFor(activationState),
|
|
238
|
+
applyBlocked: activationState === "root_partial",
|
|
239
|
+
discoverySummary: {
|
|
240
|
+
markdownFiles: discovery.markdownFiles.length,
|
|
241
|
+
manifestFiles: discovery.manifestFiles.length,
|
|
242
|
+
vendored: activationDiscovery.summary.vendored ?? [],
|
|
243
|
+
},
|
|
244
|
+
candidates: candidateRecords,
|
|
245
|
+
suppressedCandidates: suppressed,
|
|
246
|
+
payoffSummary: (() => {
|
|
247
|
+
// current counts by type
|
|
248
|
+
const current = {};
|
|
249
|
+
try {
|
|
250
|
+
// compute from existingIds via loadEntities would be expensive; fall back to empty
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
// noop
|
|
254
|
+
}
|
|
255
|
+
// projected if all applied
|
|
256
|
+
const projected = { ...current };
|
|
257
|
+
for (const r of candidateRecords) {
|
|
258
|
+
const t = String(r.entityType || "unknown");
|
|
259
|
+
projected[t] = (projected[t] || 0) + 1;
|
|
260
|
+
}
|
|
261
|
+
const delta = {};
|
|
262
|
+
for (const k of Object.keys(projected)) {
|
|
263
|
+
const projectedValue = projected[k] ?? 0;
|
|
264
|
+
const currentValue = current[k] ?? 0;
|
|
265
|
+
delta[k] = projectedValue - currentValue;
|
|
266
|
+
}
|
|
267
|
+
return { current, projectedIfAllApplied: projected, delta };
|
|
268
|
+
})(),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|