kibi-mcp 0.6.0 → 0.7.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/diagnostics.js +11 -6
- package/dist/env.js +31 -1
- package/dist/server/docs.js +3 -0
- package/dist/server/session.js +55 -19
- package/dist/server/tools.js +119 -71
- package/dist/server/transport.js +2 -1
- package/dist/tools/check.js +12 -15
- package/dist/tools/core-module.js +15 -9
- package/dist/tools/coverage.js +1 -1
- package/dist/tools/find-gaps.js +1 -1
- package/dist/tools/graph.js +1 -1
- package/dist/tools/prolog-list.js +5 -1
- package/dist/tools/query.js +7 -1
- package/dist/tools/search.js +3 -1
- package/dist/tools/symbols.js +95 -34
- package/dist/tools/upsert.js +8 -2
- package/dist/tools-config.js +5 -1
- package/package.json +4 -7
package/dist/diagnostics.js
CHANGED
|
@@ -33,19 +33,24 @@ let diagnosticUsageLogPath = null;
|
|
|
33
33
|
* Initialize diagnostic mode: set up usage.log path.
|
|
34
34
|
* Called once during server startup.
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
process.env
|
|
36
|
+
// implements REQ-008
|
|
37
|
+
export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
|
|
38
|
+
diagnosticUsageLogPath = null;
|
|
39
|
+
if (!enabled) {
|
|
40
|
+
Reflect.deleteProperty(process.env, "KIBI_MCP_DIAGNOSTIC_MODE");
|
|
41
|
+
return;
|
|
41
42
|
}
|
|
43
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
44
|
+
diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
|
|
45
|
+
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
48
|
* Append a JSON line to the usage.log file.
|
|
45
49
|
* No-op if diagnostic mode is not enabled.
|
|
46
50
|
*/
|
|
51
|
+
// implements REQ-008
|
|
47
52
|
export function appendUsageLogLine(entry) {
|
|
48
|
-
if (!
|
|
53
|
+
if (!diagnosticUsageLogPath) {
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
51
56
|
const logDir = path.dirname(diagnosticUsageLogPath);
|
package/dist/env.js
CHANGED
|
@@ -18,8 +18,38 @@
|
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import { resolveEnvFilePath, resolveWorkspaceRoot } from "./workspace.js";
|
|
20
20
|
const DEFAULT_ENV_FILE = ".env";
|
|
21
|
+
function getEnvValue(key) {
|
|
22
|
+
const value = process.env[key];
|
|
23
|
+
return typeof value === "string" ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function getTrimmedEnvValue(key) {
|
|
26
|
+
const value = getEnvValue(key)?.trim();
|
|
27
|
+
return value ? value : undefined;
|
|
28
|
+
}
|
|
29
|
+
export function getEnvFileName() {
|
|
30
|
+
// implements REQ-002
|
|
31
|
+
return getTrimmedEnvValue("KIBI_ENV_FILE") ?? DEFAULT_ENV_FILE;
|
|
32
|
+
}
|
|
33
|
+
export function isMcpDebugEnabled() {
|
|
34
|
+
// implements REQ-002
|
|
35
|
+
return Boolean(getEnvValue("KIBI_MCP_DEBUG"));
|
|
36
|
+
}
|
|
37
|
+
export function getBranchOverride() {
|
|
38
|
+
// implements REQ-002
|
|
39
|
+
return getTrimmedEnvValue("KIBI_BRANCH");
|
|
40
|
+
}
|
|
41
|
+
export function getKbPlPathOverride() {
|
|
42
|
+
// implements REQ-002
|
|
43
|
+
return getEnvValue("KIBI_KB_PL_PATH");
|
|
44
|
+
}
|
|
45
|
+
export function getCoreModulePathOverride(fileName) {
|
|
46
|
+
// implements REQ-002
|
|
47
|
+
const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
|
|
48
|
+
return getEnvValue(envKey);
|
|
49
|
+
}
|
|
21
50
|
export function loadDefaultEnvFile() {
|
|
22
|
-
|
|
51
|
+
// implements REQ-002
|
|
52
|
+
const envFileName = getEnvFileName();
|
|
23
53
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
24
54
|
return loadEnvFile({ envFileName, workspaceRoot });
|
|
25
55
|
}
|
package/dist/server/docs.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import { TOOLS } from "../tools-config.js";
|
|
19
|
+
// INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the runtime
|
|
20
|
+
// interface with looser Record<string, unknown> inputSchema. The cast is safe because the
|
|
21
|
+
// tool definitions are statically authored and validated at startup.
|
|
19
22
|
const ACTIVE_TOOLS = TOOLS;
|
|
20
23
|
function renderToolsDoc() {
|
|
21
24
|
const lines = [
|
package/dist/server/session.js
CHANGED
|
@@ -20,7 +20,20 @@ import { createRequire } from "node:module";
|
|
|
20
20
|
import process from "node:process";
|
|
21
21
|
import { PrologProcess } from "kibi-cli/prolog";
|
|
22
22
|
import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
|
|
23
|
+
import { getBranchOverride, isMcpDebugEnabled } from "../env.js";
|
|
23
24
|
import { resolveKbPath, resolveWorkspaceRoot } from "../workspace.js";
|
|
25
|
+
const defaultSessionDeps = {
|
|
26
|
+
PrologProcess,
|
|
27
|
+
copyCleanSnapshot,
|
|
28
|
+
createRequire,
|
|
29
|
+
fs,
|
|
30
|
+
getBranchDiagnostic,
|
|
31
|
+
isValidBranchName,
|
|
32
|
+
resolveActiveBranch,
|
|
33
|
+
resolveKbPath,
|
|
34
|
+
resolveWorkspaceRoot,
|
|
35
|
+
};
|
|
36
|
+
let sessionDeps = { ...defaultSessionDeps };
|
|
24
37
|
export let prologProcess = null;
|
|
25
38
|
let isInitialized = false;
|
|
26
39
|
export let activeBranchName = "develop";
|
|
@@ -28,32 +41,55 @@ let ensurePrologTail = Promise.resolve();
|
|
|
28
41
|
export let isShuttingDown = false;
|
|
29
42
|
let shutdownTimeout = null;
|
|
30
43
|
export const inFlightRequests = new Map();
|
|
44
|
+
// implements REQ-008
|
|
45
|
+
export function resetSessionStateForTests() {
|
|
46
|
+
prologProcess = null;
|
|
47
|
+
isInitialized = false;
|
|
48
|
+
activeBranchName = "develop";
|
|
49
|
+
ensurePrologTail = Promise.resolve();
|
|
50
|
+
isShuttingDown = false;
|
|
51
|
+
inFlightRequests.clear();
|
|
52
|
+
if (shutdownTimeout) {
|
|
53
|
+
clearTimeout(shutdownTimeout);
|
|
54
|
+
shutdownTimeout = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function _setSessionDepsForTests(
|
|
58
|
+
// implements REQ-008
|
|
59
|
+
overrides) {
|
|
60
|
+
sessionDeps = { ...sessionDeps, ...overrides };
|
|
61
|
+
}
|
|
62
|
+
export function _resetSessionDepsForTests() {
|
|
63
|
+
// implements REQ-008
|
|
64
|
+
sessionDeps = { ...defaultSessionDeps };
|
|
65
|
+
}
|
|
31
66
|
function debugLog(...args) {
|
|
32
|
-
if (
|
|
67
|
+
if (isMcpDebugEnabled()) {
|
|
33
68
|
console.error(...args);
|
|
34
69
|
}
|
|
35
70
|
}
|
|
36
71
|
export function ensureBranchKbExists(workspaceRoot, branch) {
|
|
37
|
-
|
|
72
|
+
// implements REQ-008
|
|
73
|
+
if (!sessionDeps.isValidBranchName(branch)) {
|
|
38
74
|
throw new Error(`Invalid branch name: ${branch}`);
|
|
39
75
|
}
|
|
40
|
-
const branchPath = resolveKbPath(workspaceRoot, branch);
|
|
41
|
-
if (fs.existsSync(branchPath)) {
|
|
76
|
+
const branchPath = sessionDeps.resolveKbPath(workspaceRoot, branch);
|
|
77
|
+
if (sessionDeps.fs.existsSync(branchPath)) {
|
|
42
78
|
return;
|
|
43
79
|
}
|
|
44
80
|
// Try to copy from the previously active branch if available
|
|
45
81
|
const previousBranch = activeBranchName;
|
|
46
|
-
const previousBranchPath = resolveKbPath(workspaceRoot, previousBranch);
|
|
82
|
+
const previousBranchPath = sessionDeps.resolveKbPath(workspaceRoot, previousBranch);
|
|
47
83
|
if (previousBranch !== branch &&
|
|
48
84
|
previousBranch !== "develop" &&
|
|
49
|
-
fs.existsSync(previousBranchPath)) {
|
|
85
|
+
sessionDeps.fs.existsSync(previousBranchPath)) {
|
|
50
86
|
// Copy from previous branch for continuity
|
|
51
|
-
copyCleanSnapshot(previousBranchPath, branchPath);
|
|
87
|
+
sessionDeps.copyCleanSnapshot(previousBranchPath, branchPath);
|
|
52
88
|
debugLog(`[KIBI-MCP] Created branch KB for '${branch}' from '${previousBranch}'`);
|
|
53
89
|
return;
|
|
54
90
|
}
|
|
55
91
|
// No previous branch available - create empty KB
|
|
56
|
-
fs.mkdirSync(branchPath, { recursive: true });
|
|
92
|
+
sessionDeps.fs.mkdirSync(branchPath, { recursive: true });
|
|
57
93
|
debugLog(`[KIBI-MCP] Created empty branch KB for '${branch}'`);
|
|
58
94
|
}
|
|
59
95
|
export async function initiateGracefulShutdown(exitCode = 0) {
|
|
@@ -103,22 +139,22 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
103
139
|
}
|
|
104
140
|
// implements REQ-008
|
|
105
141
|
async function ensurePrologUnsafe() {
|
|
106
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
142
|
+
const workspaceRoot = sessionDeps.resolveWorkspaceRoot();
|
|
107
143
|
// Determine target branch: respect KIBI_BRANCH override or resolve from git
|
|
108
|
-
const envBranch =
|
|
144
|
+
const envBranch = getBranchOverride();
|
|
109
145
|
let targetBranch;
|
|
110
146
|
if (envBranch) {
|
|
111
147
|
// KIBI_BRANCH override is set - use it without re-resolving from git
|
|
112
|
-
if (!isValidBranchName(envBranch)) {
|
|
148
|
+
if (!sessionDeps.isValidBranchName(envBranch)) {
|
|
113
149
|
throw new Error(`Invalid branch name from KIBI_BRANCH: '${envBranch}'`);
|
|
114
150
|
}
|
|
115
151
|
targetBranch = envBranch;
|
|
116
152
|
}
|
|
117
153
|
else {
|
|
118
154
|
// No override - resolve active branch from git (may change between requests)
|
|
119
|
-
const branchResult = resolveActiveBranch(workspaceRoot);
|
|
155
|
+
const branchResult = sessionDeps.resolveActiveBranch(workspaceRoot);
|
|
120
156
|
if ("error" in branchResult) {
|
|
121
|
-
const diagnostic = getBranchDiagnostic(undefined, branchResult.error);
|
|
157
|
+
const diagnostic = sessionDeps.getBranchDiagnostic(undefined, branchResult.error);
|
|
122
158
|
console.error(`[KIBI-MCP] ${diagnostic}`);
|
|
123
159
|
throw new Error(`Failed to resolve active branch: ${branchResult.error}`);
|
|
124
160
|
}
|
|
@@ -144,7 +180,7 @@ async function ensurePrologUnsafe() {
|
|
|
144
180
|
}
|
|
145
181
|
// Ensure new branch KB exists
|
|
146
182
|
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
147
|
-
const newKbPath = resolveKbPath(workspaceRoot, targetBranch);
|
|
183
|
+
const newKbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
148
184
|
// Attach to new branch KB
|
|
149
185
|
const attachResult = await prologProcess.query(`kb_attach('${newKbPath}')`);
|
|
150
186
|
if (!attachResult.success) {
|
|
@@ -157,13 +193,13 @@ async function ensurePrologUnsafe() {
|
|
|
157
193
|
}
|
|
158
194
|
// First initialization
|
|
159
195
|
debugLog("[KIBI-MCP] Initializing Prolog process...");
|
|
160
|
-
prologProcess = new PrologProcess({ timeout: 120000 });
|
|
196
|
+
prologProcess = new sessionDeps.PrologProcess({ timeout: 120000 });
|
|
161
197
|
await prologProcess.start();
|
|
162
198
|
// Startup debug: resolve which kibi-cli is being used and its version (best-effort).
|
|
163
199
|
// Gate all output under KIBI_MCP_DEBUG and write only to stderr via debugLog.
|
|
164
|
-
if (
|
|
200
|
+
if (isMcpDebugEnabled()) {
|
|
165
201
|
try {
|
|
166
|
-
const req = createRequire(import.meta.url);
|
|
202
|
+
const req = sessionDeps.createRequire(import.meta.url);
|
|
167
203
|
try {
|
|
168
204
|
const resolved = req.resolve("kibi-cli/prolog");
|
|
169
205
|
debugLog(`[KIBI-MCP] require.resolve('kibi-cli/prolog') -> ${resolved}`);
|
|
@@ -193,11 +229,11 @@ async function ensurePrologUnsafe() {
|
|
|
193
229
|
}
|
|
194
230
|
}
|
|
195
231
|
debugLog("[KIBI-MCP] Branch selection:");
|
|
196
|
-
debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${
|
|
232
|
+
debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${envBranch || "not set"}`);
|
|
197
233
|
debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
|
|
198
234
|
activeBranchName = targetBranch;
|
|
199
235
|
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
200
|
-
const kbPath = resolveKbPath(workspaceRoot, targetBranch);
|
|
236
|
+
const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
201
237
|
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
202
238
|
if (!attachResult.success) {
|
|
203
239
|
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
package/dist/server/tools.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
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 process from "node:process";
|
|
19
1
|
import { z } from "zod";
|
|
20
2
|
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
3
|
+
import { isMcpDebugEnabled } from "../env.js";
|
|
21
4
|
import { TOOLS } from "../tools-config.js";
|
|
22
5
|
import { handleKbCheck } from "../tools/check.js";
|
|
23
6
|
import { handleKbCoverage } from "../tools/coverage.js";
|
|
@@ -28,13 +11,60 @@ import { handleKbQuery } from "../tools/query.js";
|
|
|
28
11
|
import { handleKbSearch } from "../tools/search.js";
|
|
29
12
|
import { handleKbStatus } from "../tools/status.js";
|
|
30
13
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
const defaultToolsServerDeps = {
|
|
15
|
+
getSessionModule: () => import("./session.js"),
|
|
16
|
+
};
|
|
17
|
+
// implements REQ-008
|
|
18
|
+
export function _setToolsServerDepsForTests(deps, resetPromise = false) {
|
|
19
|
+
defaultToolsServerDeps.getSessionModule =
|
|
20
|
+
deps.getSessionModule ?? defaultToolsServerDeps.getSessionModule;
|
|
21
|
+
if (resetPromise) {
|
|
22
|
+
sessionModulePromise = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// implements REQ-012
|
|
26
|
+
export function _resetSessionModulePromise() {
|
|
27
|
+
sessionModulePromise = null;
|
|
28
|
+
}
|
|
29
|
+
let sessionModulePromise = null;
|
|
30
|
+
/* v8 ignore next (3 lines) — lazy async module loader; body only executes once per process
|
|
31
|
+
* when DEFAULT_TOOLS_RUNTIME.activeBranchName/ensureProlog/etc. are first called.
|
|
32
|
+
* Cannot be re-triggered without process restart (sessionModulePromise is module-level). */
|
|
33
|
+
async function getSessionModule() {
|
|
34
|
+
sessionModulePromise ??= defaultToolsServerDeps.getSessionModule();
|
|
35
|
+
return sessionModulePromise;
|
|
36
|
+
}
|
|
37
|
+
const DEFAULT_TOOLS_RUNTIME = {
|
|
38
|
+
diagnosticModeEnabled: () => DIAGNOSTIC_MODE_ENABLED,
|
|
39
|
+
appendUsageLogLine,
|
|
40
|
+
deriveDiagnosticFields,
|
|
41
|
+
extractToolCallPayload,
|
|
42
|
+
// INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the
|
|
43
|
+
// runtime interface with looser Record<string, unknown> inputSchema. The cast is safe
|
|
44
|
+
// because the tool definitions are statically authored and validated at startup.
|
|
45
|
+
tools: TOOLS,
|
|
46
|
+
activeBranchName: async () => (await getSessionModule()).activeBranchName,
|
|
47
|
+
ensureProlog: async () => (await getSessionModule()).ensureProlog(),
|
|
48
|
+
inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
|
|
49
|
+
isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
|
|
50
|
+
prologProcess: async () => (await getSessionModule()).prologProcess,
|
|
51
|
+
handleKbCheck,
|
|
52
|
+
handleKbCoverage,
|
|
53
|
+
handleKbDelete,
|
|
54
|
+
handleKbFindGaps,
|
|
55
|
+
handleKbGraph,
|
|
56
|
+
handleKbQuery,
|
|
57
|
+
handleKbSearch,
|
|
58
|
+
handleKbStatus,
|
|
59
|
+
handleKbUpsert,
|
|
60
|
+
};
|
|
61
|
+
// implements REQ-008
|
|
33
62
|
function debugLog(...args) {
|
|
34
|
-
if (
|
|
63
|
+
if (isMcpDebugEnabled()) {
|
|
35
64
|
console.error(...args);
|
|
36
65
|
}
|
|
37
66
|
}
|
|
67
|
+
// implements REQ-002
|
|
38
68
|
export function jsonSchemaToZod(schema) {
|
|
39
69
|
if (!schema || typeof schema !== "object") {
|
|
40
70
|
return z.any();
|
|
@@ -52,6 +82,9 @@ export function jsonSchemaToZod(schema) {
|
|
|
52
82
|
const literalSchemas = literals.map((value) => z.literal(value));
|
|
53
83
|
if (literalSchemas.length === 1) {
|
|
54
84
|
const single = literalSchemas[0];
|
|
85
|
+
if (!single) {
|
|
86
|
+
return description ? z.any().describe(description) : z.any();
|
|
87
|
+
}
|
|
55
88
|
return description ? single.describe(description) : single;
|
|
56
89
|
}
|
|
57
90
|
const union = z.union(literalSchemas);
|
|
@@ -71,10 +104,9 @@ export function jsonSchemaToZod(schema) {
|
|
|
71
104
|
const propSchema = jsonSchemaToZod(value);
|
|
72
105
|
shape[key] = required.has(key) ? propSchema : propSchema.optional();
|
|
73
106
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
107
|
+
const objectSchema = obj.additionalProperties === false
|
|
108
|
+
? z.object(shape)
|
|
109
|
+
: z.looseObject(shape);
|
|
78
110
|
const description = typeof obj.description === "string" ? obj.description : undefined;
|
|
79
111
|
return description ? objectSchema.describe(description) : objectSchema;
|
|
80
112
|
}
|
|
@@ -135,12 +167,17 @@ export function jsonSchemaToZod(schema) {
|
|
|
135
167
|
}
|
|
136
168
|
}
|
|
137
169
|
}
|
|
138
|
-
|
|
170
|
+
// implements REQ-002
|
|
171
|
+
export function addTool(server, name, description, inputSchema, handler,
|
|
172
|
+
// INTENTIONAL: DEFAULT_TOOLS_RUNTIME is typed as ToolsRuntime<PrologProcess>; the
|
|
173
|
+
// generic TProlog parameter exists so tests can inject a mock type. The cast is safe
|
|
174
|
+
// because the runtime object satisfies the full ToolsRuntime contract at runtime.
|
|
175
|
+
runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
139
176
|
const wrappedHandler = async (args) => {
|
|
140
177
|
const startedAt = new Date();
|
|
141
|
-
|
|
142
|
-
const { businessArgs, telemetry } =
|
|
143
|
-
? extractToolCallPayload(args)
|
|
178
|
+
const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
|
|
179
|
+
const { businessArgs, telemetry } = diagnosticModeEnabled
|
|
180
|
+
? runtime.extractToolCallPayload(args)
|
|
144
181
|
: { businessArgs: args, telemetry: null };
|
|
145
182
|
try {
|
|
146
183
|
// Validate that args is a valid object
|
|
@@ -148,7 +185,7 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
148
185
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
149
186
|
}
|
|
150
187
|
// Check if shutting down before processing
|
|
151
|
-
if (isShuttingDown) {
|
|
188
|
+
if (await runtime.isShuttingDown()) {
|
|
152
189
|
throw new Error(`Tool ${name} rejected: server is shutting down`);
|
|
153
190
|
}
|
|
154
191
|
// Extract or generate requestId from args
|
|
@@ -157,20 +194,23 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
157
194
|
? requestIdArg
|
|
158
195
|
: `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
159
196
|
// Log tool call for debugging (to stderr to avoid breaking stdio protocol)
|
|
160
|
-
if (
|
|
197
|
+
if (isMcpDebugEnabled()) {
|
|
161
198
|
console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(businessArgs));
|
|
162
199
|
}
|
|
163
200
|
// Track the handler promise in inFlightRequests Map
|
|
201
|
+
const trackedRequests = await runtime.inFlightRequests();
|
|
164
202
|
const handlerPromise = handler(businessArgs);
|
|
165
|
-
|
|
203
|
+
trackedRequests.set(requestId, handlerPromise);
|
|
166
204
|
try {
|
|
167
205
|
// Execute handler
|
|
168
206
|
const result = await handlerPromise;
|
|
169
207
|
// Log usage in diagnostic mode
|
|
170
|
-
if (
|
|
208
|
+
if (diagnosticModeEnabled) {
|
|
171
209
|
const finishedAt = new Date();
|
|
172
|
-
const diagnosticFields = deriveDiagnosticFields(name, businessArgs, telemetry, result);
|
|
173
|
-
|
|
210
|
+
const diagnosticFields = runtime.deriveDiagnosticFields(name, businessArgs, telemetry, result);
|
|
211
|
+
const processHandle = await runtime.prologProcess();
|
|
212
|
+
const branchName = await runtime.activeBranchName();
|
|
213
|
+
runtime.appendUsageLogLine({
|
|
174
214
|
timestamp: finishedAt.toISOString(),
|
|
175
215
|
request_id: requestId,
|
|
176
216
|
tool: name,
|
|
@@ -180,8 +220,8 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
180
220
|
started_at: startedAt.toISOString(),
|
|
181
221
|
finished_at: finishedAt.toISOString(),
|
|
182
222
|
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
183
|
-
prolog_pid:
|
|
184
|
-
active_branch:
|
|
223
|
+
prolog_pid: processHandle?.getPid() ?? null,
|
|
224
|
+
active_branch: branchName,
|
|
185
225
|
...diagnosticFields,
|
|
186
226
|
});
|
|
187
227
|
}
|
|
@@ -189,10 +229,12 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
189
229
|
}
|
|
190
230
|
catch (error) {
|
|
191
231
|
// Log error in diagnostic mode
|
|
192
|
-
if (
|
|
232
|
+
if (diagnosticModeEnabled) {
|
|
193
233
|
const finishedAt = new Date();
|
|
194
234
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
195
|
-
|
|
235
|
+
const processHandle = await runtime.prologProcess();
|
|
236
|
+
const branchName = await runtime.activeBranchName();
|
|
237
|
+
runtime.appendUsageLogLine({
|
|
196
238
|
timestamp: finishedAt.toISOString(),
|
|
197
239
|
request_id: requestId,
|
|
198
240
|
tool: name,
|
|
@@ -202,8 +244,8 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
202
244
|
started_at: startedAt.toISOString(),
|
|
203
245
|
finished_at: finishedAt.toISOString(),
|
|
204
246
|
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
205
|
-
prolog_pid:
|
|
206
|
-
active_branch:
|
|
247
|
+
prolog_pid: processHandle?.getPid() ?? null,
|
|
248
|
+
active_branch: branchName,
|
|
207
249
|
error_message: err.message,
|
|
208
250
|
});
|
|
209
251
|
}
|
|
@@ -211,7 +253,7 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
211
253
|
}
|
|
212
254
|
finally {
|
|
213
255
|
// Always clean up from Map when done (success or failure)
|
|
214
|
-
|
|
256
|
+
trackedRequests.delete(requestId);
|
|
215
257
|
}
|
|
216
258
|
}
|
|
217
259
|
catch (error) {
|
|
@@ -226,47 +268,53 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
226
268
|
server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
|
|
227
269
|
}
|
|
228
270
|
// implements REQ-002, REQ-013
|
|
229
|
-
export function registerAllTools(server
|
|
271
|
+
export function registerAllTools(server,
|
|
272
|
+
// INTENTIONAL: same generic bridge cast as addTool — see comment there.
|
|
273
|
+
runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
230
274
|
const toolDef = (name) => {
|
|
231
|
-
const t =
|
|
275
|
+
const t = runtime.tools.find((tool) => tool.name === name);
|
|
232
276
|
if (!t)
|
|
233
277
|
throw new Error(`Unknown tool: ${name}`);
|
|
234
278
|
return t;
|
|
235
279
|
};
|
|
280
|
+
// INTENTIONAL ARGUMENT CASTS: The `args as (unknown as)? XyzArgs` casts below
|
|
281
|
+
// bridge the generic ToolHandler (which receives Record<string, unknown>) to the
|
|
282
|
+
// specific handler argument types. Argument shapes are validated by Zod schemas
|
|
283
|
+
// (via jsonSchemaToZod) before the handler is invoked, so the casts are safe at runtime.
|
|
236
284
|
addTool(server, "kb_query", toolDef("kb_query").description, toolDef("kb_query").inputSchema, async (args) => {
|
|
237
|
-
const prolog = await ensureProlog();
|
|
238
|
-
return handleKbQuery(prolog, args);
|
|
239
|
-
});
|
|
285
|
+
const prolog = await runtime.ensureProlog();
|
|
286
|
+
return runtime.handleKbQuery(prolog, args);
|
|
287
|
+
}, runtime);
|
|
240
288
|
addTool(server, "kb_search", toolDef("kb_search").description, toolDef("kb_search").inputSchema, async (args) => {
|
|
241
|
-
const prolog = await ensureProlog();
|
|
242
|
-
return handleKbSearch(prolog, args);
|
|
243
|
-
});
|
|
289
|
+
const prolog = await runtime.ensureProlog();
|
|
290
|
+
return runtime.handleKbSearch(prolog, args);
|
|
291
|
+
}, runtime);
|
|
244
292
|
addTool(server, "kb_status", toolDef("kb_status").description, toolDef("kb_status").inputSchema, async (args) => {
|
|
245
|
-
const prolog = await ensureProlog();
|
|
246
|
-
return handleKbStatus(prolog, args);
|
|
247
|
-
});
|
|
293
|
+
const prolog = await runtime.ensureProlog();
|
|
294
|
+
return runtime.handleKbStatus(prolog, args);
|
|
295
|
+
}, runtime);
|
|
248
296
|
addTool(server, "kb_find_gaps", toolDef("kb_find_gaps").description, toolDef("kb_find_gaps").inputSchema, async (args) => {
|
|
249
|
-
const prolog = await ensureProlog();
|
|
250
|
-
return handleKbFindGaps(prolog, args);
|
|
251
|
-
});
|
|
297
|
+
const prolog = await runtime.ensureProlog();
|
|
298
|
+
return runtime.handleKbFindGaps(prolog, args);
|
|
299
|
+
}, runtime);
|
|
252
300
|
addTool(server, "kb_coverage", toolDef("kb_coverage").description, toolDef("kb_coverage").inputSchema, async (args) => {
|
|
253
|
-
const prolog = await ensureProlog();
|
|
254
|
-
return handleKbCoverage(prolog, args);
|
|
255
|
-
});
|
|
301
|
+
const prolog = await runtime.ensureProlog();
|
|
302
|
+
return runtime.handleKbCoverage(prolog, args);
|
|
303
|
+
}, runtime);
|
|
256
304
|
addTool(server, "kb_graph", toolDef("kb_graph").description, toolDef("kb_graph").inputSchema, async (args) => {
|
|
257
|
-
const prolog = await ensureProlog();
|
|
258
|
-
return handleKbGraph(prolog, args);
|
|
259
|
-
});
|
|
305
|
+
const prolog = await runtime.ensureProlog();
|
|
306
|
+
return runtime.handleKbGraph(prolog, args);
|
|
307
|
+
}, runtime);
|
|
260
308
|
addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
|
|
261
|
-
const prolog = await ensureProlog();
|
|
262
|
-
return handleKbUpsert(prolog, args);
|
|
263
|
-
});
|
|
309
|
+
const prolog = await runtime.ensureProlog();
|
|
310
|
+
return runtime.handleKbUpsert(prolog, args);
|
|
311
|
+
}, runtime);
|
|
264
312
|
addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
|
|
265
|
-
const prolog = await ensureProlog();
|
|
266
|
-
return handleKbDelete(prolog, args);
|
|
267
|
-
});
|
|
313
|
+
const prolog = await runtime.ensureProlog();
|
|
314
|
+
return runtime.handleKbDelete(prolog, args);
|
|
315
|
+
}, runtime);
|
|
268
316
|
addTool(server, "kb_check", toolDef("kb_check").description, toolDef("kb_check").inputSchema, async (args) => {
|
|
269
|
-
const prolog = await ensureProlog();
|
|
270
|
-
return handleKbCheck(prolog, args);
|
|
271
|
-
});
|
|
317
|
+
const prolog = await runtime.ensureProlog();
|
|
318
|
+
return runtime.handleKbCheck(prolog, args);
|
|
319
|
+
}, runtime);
|
|
272
320
|
}
|
package/dist/server/transport.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import process from "node:process";
|
|
19
|
+
import { isMcpDebugEnabled } from "../env.js";
|
|
19
20
|
import { initiateGracefulShutdown } from "./session.js";
|
|
20
21
|
function debugLog(...args) {
|
|
21
|
-
if (
|
|
22
|
+
if (isMcpDebugEnabled()) {
|
|
22
23
|
console.error(...args);
|
|
23
24
|
}
|
|
24
25
|
}
|
package/dist/tools/check.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { readFile } from "node:fs/promises";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
21
21
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
@@ -25,8 +25,8 @@ function formatDiagnosticsForMcp(diagnostics) {
|
|
|
25
25
|
category: d.category,
|
|
26
26
|
severity: d.severity,
|
|
27
27
|
message: d.message,
|
|
28
|
-
file: d.file,
|
|
29
|
-
suggestion: d.suggestion,
|
|
28
|
+
...(d.file !== undefined ? { file: d.file } : {}),
|
|
29
|
+
...(d.suggestion !== undefined ? { suggestion: d.suggestion } : {}),
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
32
|
function formatViolationText(violations) {
|
|
@@ -48,13 +48,10 @@ function formatViolationText(violations) {
|
|
|
48
48
|
return `${violations.length} violations found\n${details.join("\n")}`;
|
|
49
49
|
}
|
|
50
50
|
// implements REQ-002
|
|
51
|
-
function loadChecksConfig(workspaceRoot) {
|
|
51
|
+
async function loadChecksConfig(workspaceRoot) {
|
|
52
52
|
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
53
|
-
if (!existsSync(configPath)) {
|
|
54
|
-
return DEFAULT_CHECKS_CONFIG;
|
|
55
|
-
}
|
|
56
53
|
try {
|
|
57
|
-
const content =
|
|
54
|
+
const content = await readFile(configPath, "utf8");
|
|
58
55
|
const parsed = JSON.parse(content);
|
|
59
56
|
const parsedRules = parsed.checks?.rules;
|
|
60
57
|
const normalizedRules = {
|
|
@@ -104,10 +101,10 @@ function getEffectiveRules(configRules, requestedRules) {
|
|
|
104
101
|
*/
|
|
105
102
|
// implements REQ-002
|
|
106
103
|
export async function handleKbCheck(prolog, args) {
|
|
107
|
-
const { rules } = args;
|
|
104
|
+
const { rules, workspaceRoot: workspaceOverride } = args;
|
|
108
105
|
try {
|
|
109
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
110
|
-
const checksConfig = loadChecksConfig(workspaceRoot);
|
|
106
|
+
const workspaceRoot = workspaceOverride ?? resolveWorkspaceRoot();
|
|
107
|
+
const checksConfig = await loadChecksConfig(workspaceRoot);
|
|
111
108
|
const rulesAllowlist = getEffectiveRules(checksConfig.rules, rules);
|
|
112
109
|
if (rulesAllowlist.size === 0) {
|
|
113
110
|
return {
|
|
@@ -126,8 +123,8 @@ export async function handleKbCheck(prolog, args) {
|
|
|
126
123
|
category: "SYNC_ERROR",
|
|
127
124
|
severity: "error",
|
|
128
125
|
message: v.description,
|
|
129
|
-
file: v.source,
|
|
130
|
-
suggestion: v.suggestion,
|
|
126
|
+
...(v.source !== undefined ? { file: v.source } : {}),
|
|
127
|
+
...(v.suggestion !== undefined ? { suggestion: v.suggestion } : {}),
|
|
131
128
|
}));
|
|
132
129
|
const summary = formatViolationText(aggregatedViolations);
|
|
133
130
|
return {
|
|
@@ -189,8 +186,8 @@ async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
|
|
|
189
186
|
rule: v.rule,
|
|
190
187
|
entityId: v.entityId,
|
|
191
188
|
description: v.description,
|
|
192
|
-
suggestion: v.suggestion
|
|
193
|
-
source: v.source
|
|
189
|
+
...(v.suggestion ? { suggestion: v.suggestion } : {}),
|
|
190
|
+
...(v.source ? { source: v.source } : {}),
|
|
194
191
|
});
|
|
195
192
|
}
|
|
196
193
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { PrologProcess, resolveKbPlPath } from "kibi-cli/prolog";
|
|
4
3
|
import { escapeAtomContent } from "kibi-cli/prolog/codec";
|
|
4
|
+
import { getCoreModulePathOverride, getKbPlPathOverride } from "../env.js";
|
|
5
5
|
// implements REQ-002, REQ-013
|
|
6
6
|
export function resolveCorePlPath(fileName) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
if (override && existsSync(override)) {
|
|
7
|
+
const override = getCoreModulePathOverride(fileName);
|
|
8
|
+
if (override) {
|
|
10
9
|
return override;
|
|
11
10
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Fall back to the generic KB_PL override so test fixtures
|
|
12
|
+
// (which set only KIBI_KB_PL_PATH) can still resolve sibling modules.
|
|
13
|
+
const genericOverride = getKbPlPathOverride();
|
|
14
|
+
if (genericOverride) {
|
|
15
|
+
return path.join(path.dirname(genericOverride), fileName);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
const kbPlPath = resolveKbPlPath();
|
|
18
|
+
return path.join(path.dirname(kbPlPath), fileName);
|
|
18
19
|
}
|
|
19
20
|
// implements REQ-002, REQ-013
|
|
20
21
|
export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel) {
|
|
@@ -34,6 +35,11 @@ export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel) {
|
|
|
34
35
|
}
|
|
35
36
|
return mockedParsed;
|
|
36
37
|
}
|
|
38
|
+
// NOTE: useOneShotMode is an internal optimization flag on PrologProcess that
|
|
39
|
+
// forces single-query mode (start → query → terminate per call) instead of the
|
|
40
|
+
// default interactive session. It is not exposed in the public PrologProcess type
|
|
41
|
+
// because callers should not set it directly — only internal discovery helpers
|
|
42
|
+
// use it for lightweight one-shot queries that don't need session state.
|
|
37
43
|
const oneShotCapable = prolog;
|
|
38
44
|
prolog.invalidateCache();
|
|
39
45
|
const result = oneShotCapable.useOneShotMode
|
package/dist/tools/coverage.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function handleKbCoverage(prolog, args) {
|
|
|
18
18
|
text: `Coverage summary: ${fullyCovered} fully covered out of ${total}.`,
|
|
19
19
|
},
|
|
20
20
|
],
|
|
21
|
-
structuredContent: payload,
|
|
21
|
+
...(payload !== undefined ? { structuredContent: payload } : {}),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
catch (error) {
|
package/dist/tools/find-gaps.js
CHANGED
package/dist/tools/graph.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function handleKbGraph(prolog, args) {
|
|
|
17
17
|
: `Graph traversal returned ${nodes.length} nodes and ${(payload?.edges ?? []).length} edges from ${args.seedIds.join(", ")}.`,
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
|
-
structuredContent: payload,
|
|
20
|
+
...(payload !== undefined ? { structuredContent: payload } : {}),
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
@@ -37,7 +37,11 @@ export function parsePairList(raw) {
|
|
|
37
37
|
for (const row of rows) {
|
|
38
38
|
const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
|
|
39
39
|
if (parts.length >= 2) {
|
|
40
|
-
|
|
40
|
+
const first = parts[0];
|
|
41
|
+
const second = parts[1];
|
|
42
|
+
if (first !== undefined && second !== undefined) {
|
|
43
|
+
pairs.push([first, second]);
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
return pairs;
|
package/dist/tools/query.js
CHANGED
|
@@ -7,7 +7,13 @@ export { VALID_ENTITY_TYPES } from "./entity-query.js";
|
|
|
7
7
|
export async function handleKbQuery(prolog, args) {
|
|
8
8
|
const { type, id, tags, sourceFile, limit = 100, offset = 0 } = args;
|
|
9
9
|
try {
|
|
10
|
-
const
|
|
10
|
+
const queryArgs = {
|
|
11
|
+
...(type !== undefined ? { type } : {}),
|
|
12
|
+
...(id !== undefined ? { id } : {}),
|
|
13
|
+
...(tags !== undefined ? { tags } : {}),
|
|
14
|
+
...(sourceFile !== undefined ? { sourceFile } : {}),
|
|
15
|
+
};
|
|
16
|
+
const results = await loadEntities(prolog, queryArgs);
|
|
11
17
|
const paginated = paginateResults(results, limit, offset);
|
|
12
18
|
// Build human-readable text with entity IDs and titles
|
|
13
19
|
let text;
|
package/dist/tools/search.js
CHANGED
|
@@ -11,7 +11,9 @@ export async function handleKbSearch(prolog, args) {
|
|
|
11
11
|
validateEntityType(type);
|
|
12
12
|
try {
|
|
13
13
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
14
|
-
const entities = await loadEntities(prolog, {
|
|
14
|
+
const entities = await loadEntities(prolog, {
|
|
15
|
+
...(type !== undefined ? { type } : {}),
|
|
16
|
+
});
|
|
15
17
|
const matches = await rankEntities(entities, trimmedQuery, workspaceRoot);
|
|
16
18
|
const paginated = paginateResults(matches, limit, offset);
|
|
17
19
|
const text = matches.length === 0
|
package/dist/tools/symbols.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { dump as dumpYAML, load as parseYAML } from "js-yaml";
|
|
21
21
|
import { enrichSymbolCoordinates, } from "kibi-cli/extractors/symbols-coordinator";
|
|
@@ -47,9 +47,9 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
47
47
|
export async function handleKbSymbolsRefresh(args) {
|
|
48
48
|
// implements REQ-vscode-traceability
|
|
49
49
|
const dryRun = args.dryRun === true;
|
|
50
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
51
|
-
const manifestPath = resolveManifestPath(workspaceRoot);
|
|
52
|
-
const rawContent =
|
|
50
|
+
const workspaceRoot = args.workspaceRoot ?? resolveWorkspaceRoot();
|
|
51
|
+
const manifestPath = await resolveManifestPath(workspaceRoot);
|
|
52
|
+
const rawContent = await readFile(manifestPath, "utf8");
|
|
53
53
|
const parsed = parseYAML(rawContent);
|
|
54
54
|
if (!isRecord(parsed) || !Array.isArray(parsed.symbols)) {
|
|
55
55
|
throw new Error(`Invalid symbols manifest at ${manifestPath}`);
|
|
@@ -63,13 +63,14 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
63
63
|
title: typeof entry.title === "string" ? entry.title : "",
|
|
64
64
|
}));
|
|
65
65
|
const enriched = await enrichSymbolCoordinates(entriesForEnrichment, workspaceRoot);
|
|
66
|
-
|
|
66
|
+
const finalized = await Promise.all(enriched.map((entry, index) => fillMissingCoordinates(original[index] ?? {}, entry, workspaceRoot)));
|
|
67
|
+
parsed.symbols = finalized;
|
|
67
68
|
let refreshed = 0;
|
|
68
69
|
let failed = 0;
|
|
69
70
|
let unchanged = 0;
|
|
70
71
|
for (let i = 0; i < original.length; i++) {
|
|
71
72
|
const before = original[i] ?? {};
|
|
72
|
-
const after =
|
|
73
|
+
const after = finalized[i] ?? before;
|
|
73
74
|
const changed = GENERATED_COORD_FIELDS.some((field) => before[field] !== after[field]);
|
|
74
75
|
if (changed) {
|
|
75
76
|
refreshed++;
|
|
@@ -80,7 +81,7 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
80
81
|
: typeof before.sourceFile === "string"
|
|
81
82
|
? before.sourceFile
|
|
82
83
|
: undefined;
|
|
83
|
-
const eligible = isEligible(source, workspaceRoot);
|
|
84
|
+
const eligible = await isEligible(source, workspaceRoot);
|
|
84
85
|
if (eligible && !hasGeneratedCoordinates(after)) {
|
|
85
86
|
failed++;
|
|
86
87
|
}
|
|
@@ -95,7 +96,7 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
95
96
|
});
|
|
96
97
|
const nextContent = `${COMMENT_BLOCK}${dumped}`;
|
|
97
98
|
if (!dryRun && rawContent !== nextContent) {
|
|
98
|
-
|
|
99
|
+
await writeFile(manifestPath, nextContent, "utf8");
|
|
99
100
|
}
|
|
100
101
|
return {
|
|
101
102
|
content: [
|
|
@@ -114,8 +115,8 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
114
115
|
}
|
|
115
116
|
export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = resolveWorkspaceRoot()) {
|
|
116
117
|
// implements REQ-vscode-traceability
|
|
117
|
-
const manifestPath = resolveManifestPath(workspaceRoot);
|
|
118
|
-
const rawContent =
|
|
118
|
+
const manifestPath = await resolveManifestPath(workspaceRoot);
|
|
119
|
+
const rawContent = await readFile(manifestPath, "utf8");
|
|
119
120
|
const parsed = parseYAML(rawContent);
|
|
120
121
|
if (!isRecord(parsed) || !Array.isArray(parsed.symbols)) {
|
|
121
122
|
return { refreshed: false, found: false };
|
|
@@ -138,9 +139,10 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
|
|
|
138
139
|
: "",
|
|
139
140
|
};
|
|
140
141
|
const [enriched] = await enrichSymbolCoordinates([singleEntry], workspaceRoot);
|
|
141
|
-
|
|
142
|
+
const finalized = await fillMissingCoordinates(original, enriched ?? singleEntry, workspaceRoot);
|
|
143
|
+
symbols[index] = finalized;
|
|
142
144
|
parsed.symbols = symbols;
|
|
143
|
-
const refreshed = GENERATED_COORD_FIELDS.some((field) => original[field] !==
|
|
145
|
+
const refreshed = GENERATED_COORD_FIELDS.some((field) => original[field] !== finalized[field]);
|
|
144
146
|
const dumped = dumpYAML(parsed, {
|
|
145
147
|
lineWidth: -1,
|
|
146
148
|
noRefs: true,
|
|
@@ -148,38 +150,41 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
|
|
|
148
150
|
});
|
|
149
151
|
const nextContent = `${COMMENT_BLOCK}${dumped}`;
|
|
150
152
|
if (rawContent !== nextContent) {
|
|
151
|
-
|
|
153
|
+
await writeFile(manifestPath, nextContent, "utf8");
|
|
152
154
|
}
|
|
153
155
|
return { refreshed, found: true };
|
|
154
156
|
}
|
|
155
|
-
export function resolveManifestPath(workspaceRoot) {
|
|
157
|
+
export async function resolveManifestPath(workspaceRoot) {
|
|
156
158
|
// implements REQ-002, REQ-013
|
|
157
159
|
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: path.resolve(workspaceRoot, config.paths.symbols);
|
|
166
|
-
}
|
|
167
|
-
// Backward compatibility: check legacy symbolsManifest field
|
|
168
|
-
if (config.symbolsManifest) {
|
|
169
|
-
return path.isAbsolute(config.symbolsManifest)
|
|
170
|
-
? config.symbolsManifest
|
|
171
|
-
: path.resolve(workspaceRoot, config.symbolsManifest);
|
|
172
|
-
}
|
|
160
|
+
try {
|
|
161
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
162
|
+
// Prefer paths.symbols (new standard) over symbolsManifest (legacy)
|
|
163
|
+
if (config.paths?.symbols) {
|
|
164
|
+
return path.isAbsolute(config.paths.symbols)
|
|
165
|
+
? config.paths.symbols
|
|
166
|
+
: path.resolve(workspaceRoot, config.paths.symbols);
|
|
173
167
|
}
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
// Backward compatibility: check legacy symbolsManifest field
|
|
169
|
+
if (config.symbolsManifest) {
|
|
170
|
+
return path.isAbsolute(config.symbolsManifest)
|
|
171
|
+
? config.symbolsManifest
|
|
172
|
+
: path.resolve(workspaceRoot, config.symbolsManifest);
|
|
176
173
|
}
|
|
177
174
|
}
|
|
175
|
+
catch {
|
|
176
|
+
// config file missing or malformed; fall through to defaults
|
|
177
|
+
}
|
|
178
178
|
const candidates = [
|
|
179
179
|
path.join(workspaceRoot, "symbols.yaml"),
|
|
180
180
|
path.join(workspaceRoot, "symbols.yml"),
|
|
181
181
|
];
|
|
182
|
-
|
|
182
|
+
for (const candidate of candidates) {
|
|
183
|
+
if (await fileExists(candidate)) {
|
|
184
|
+
return candidate;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return candidates[0] ?? path.join(workspaceRoot, "symbols.yaml");
|
|
183
188
|
}
|
|
184
189
|
function hasGeneratedCoordinates(entry) {
|
|
185
190
|
return (typeof entry.sourceLine === "number" &&
|
|
@@ -189,16 +194,72 @@ function hasGeneratedCoordinates(entry) {
|
|
|
189
194
|
typeof entry.coordinatesGeneratedAt === "string" &&
|
|
190
195
|
entry.coordinatesGeneratedAt.length > 0);
|
|
191
196
|
}
|
|
192
|
-
function isEligible(sourceFile, workspaceRoot) {
|
|
197
|
+
async function isEligible(sourceFile, workspaceRoot) {
|
|
193
198
|
if (!sourceFile)
|
|
194
199
|
return false;
|
|
195
200
|
const absolute = path.isAbsolute(sourceFile)
|
|
196
201
|
? sourceFile
|
|
197
202
|
: path.resolve(workspaceRoot, sourceFile);
|
|
198
|
-
if (!
|
|
203
|
+
if (!(await fileExists(absolute)))
|
|
199
204
|
return false;
|
|
200
205
|
return SOURCE_EXTENSIONS.has(path.extname(absolute).toLowerCase());
|
|
201
206
|
}
|
|
207
|
+
async function fileExists(filePath) {
|
|
208
|
+
try {
|
|
209
|
+
await access(filePath);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
217
|
+
if (hasGeneratedCoordinates(after)) {
|
|
218
|
+
return after;
|
|
219
|
+
}
|
|
220
|
+
const sourceFile = typeof after.sourceFile === "string"
|
|
221
|
+
? after.sourceFile
|
|
222
|
+
: typeof before.sourceFile === "string"
|
|
223
|
+
? before.sourceFile
|
|
224
|
+
: undefined;
|
|
225
|
+
const title = typeof after.title === "string"
|
|
226
|
+
? after.title
|
|
227
|
+
: typeof before.title === "string"
|
|
228
|
+
? before.title
|
|
229
|
+
: undefined;
|
|
230
|
+
if (!sourceFile || !title) {
|
|
231
|
+
return after;
|
|
232
|
+
}
|
|
233
|
+
const absolutePath = path.isAbsolute(sourceFile)
|
|
234
|
+
? sourceFile
|
|
235
|
+
: path.resolve(workspaceRoot, sourceFile);
|
|
236
|
+
try {
|
|
237
|
+
const content = await readFile(absolutePath, "utf8");
|
|
238
|
+
const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
239
|
+
const pattern = new RegExp(`\\b${escapedTitle}\\b`);
|
|
240
|
+
const lines = content.split(/\r?\n/);
|
|
241
|
+
for (let index = 0; index < lines.length; index++) {
|
|
242
|
+
const line = lines[index];
|
|
243
|
+
if (!line)
|
|
244
|
+
continue;
|
|
245
|
+
const match = pattern.exec(line);
|
|
246
|
+
if (!match || match.index < 0)
|
|
247
|
+
continue;
|
|
248
|
+
return {
|
|
249
|
+
...after,
|
|
250
|
+
sourceLine: index + 1,
|
|
251
|
+
sourceColumn: match.index,
|
|
252
|
+
sourceEndLine: index + 1,
|
|
253
|
+
sourceEndColumn: match.index + title.length,
|
|
254
|
+
coordinatesGeneratedAt: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return after;
|
|
260
|
+
}
|
|
261
|
+
return after;
|
|
262
|
+
}
|
|
202
263
|
function isRecord(value) {
|
|
203
264
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
265
|
}
|
package/dist/tools/upsert.js
CHANGED
|
@@ -19,6 +19,7 @@ import Ajv from "ajv";
|
|
|
19
19
|
import { escapeAtom, toPrologAtom, toPrologString, } from "kibi-cli/prolog/codec";
|
|
20
20
|
import entitySchema from "kibi-cli/schemas/entity";
|
|
21
21
|
import relationshipSchema from "kibi-cli/schemas/relationship";
|
|
22
|
+
import { isMcpDebugEnabled } from "../env.js";
|
|
22
23
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
23
24
|
let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
|
|
24
25
|
const ajv = new Ajv({ strict: false });
|
|
@@ -160,7 +161,7 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
160
161
|
}
|
|
161
162
|
catch (error) {
|
|
162
163
|
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
-
if (
|
|
164
|
+
if (isMcpDebugEnabled()) {
|
|
164
165
|
console.warn(`[KIBI-MCP] Symbol coordinate auto-refresh failed for ${id}: ${message}`);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
@@ -361,6 +362,9 @@ function formatUpsertError(entityId, rawError) {
|
|
|
361
362
|
if (contradictionMatch) {
|
|
362
363
|
// Extract individual conflict details from the list
|
|
363
364
|
const details = contradictionMatch[1];
|
|
365
|
+
if (!details) {
|
|
366
|
+
return `Contradiction detected for entity ${entityId}: This requirement conflicts with existing requirements. Add a supersedes relationship to the conflicting requirement, or deprecate the old requirement before creating the new one.`;
|
|
367
|
+
}
|
|
364
368
|
// Parse out readable parts - each entry is like 'Reason'-'ReqId'
|
|
365
369
|
const conflicts = [];
|
|
366
370
|
const conflictRegex = /'([^']+)'-'([^']+)'/g;
|
|
@@ -368,7 +372,9 @@ function formatUpsertError(entityId, rawError) {
|
|
|
368
372
|
while (execResult !== null) {
|
|
369
373
|
const reason = execResult[1];
|
|
370
374
|
const otherReq = execResult[2];
|
|
371
|
-
|
|
375
|
+
if (reason !== undefined && otherReq !== undefined) {
|
|
376
|
+
conflicts.push(` - Conflicts with ${otherReq}: ${reason}`);
|
|
377
|
+
}
|
|
372
378
|
execResult = conflictRegex.exec(details);
|
|
373
379
|
}
|
|
374
380
|
if (conflicts.length > 0) {
|
package/dist/tools-config.js
CHANGED
|
@@ -316,6 +316,7 @@ const BASE_TOOLS = [
|
|
|
316
316
|
"validates",
|
|
317
317
|
"implements",
|
|
318
318
|
"covered_by",
|
|
319
|
+
"executable_for",
|
|
319
320
|
"constrained_by",
|
|
320
321
|
"constrains",
|
|
321
322
|
"requires_property",
|
|
@@ -325,7 +326,7 @@ const BASE_TOOLS = [
|
|
|
325
326
|
"supersedes",
|
|
326
327
|
"relates_to",
|
|
327
328
|
],
|
|
328
|
-
description: "Relationship type enum. Use only supported values. Direction semantics follow KB model (e.g., implements symbol->req, verified_by req->test).",
|
|
329
|
+
description: "Relationship type enum. Use only supported values. Direction semantics follow KB model (e.g., implements symbol->req, verified_by req/scenario->test, executable_for symbol->test).",
|
|
329
330
|
},
|
|
330
331
|
from: {
|
|
331
332
|
type: "string",
|
|
@@ -386,6 +387,9 @@ const BASE_TOOLS = [
|
|
|
386
387
|
];
|
|
387
388
|
/**
|
|
388
389
|
* Inject _diagnostic_telemetry schema into tool inputs when diagnostic mode is enabled.
|
|
390
|
+
* TODO: This function is compile-time guarded by DIAGNOSTIC_MODE_ENABLED and only
|
|
391
|
+
* executes when the server starts with the --diagnostic-mode flag. It cannot be
|
|
392
|
+
* covered without a CLI integration test.
|
|
389
393
|
*/
|
|
390
394
|
function withDiagnosticTelemetrySchema(tools) {
|
|
391
395
|
return tools.map((tool) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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.
|
|
13
|
-
"kibi-core": "^0.
|
|
12
|
+
"kibi-cli": "^0.6.0",
|
|
13
|
+
"kibi-core": "^0.5.0",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|
|
@@ -27,10 +27,7 @@
|
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
28
|
"prepack": "npm run build"
|
|
29
29
|
},
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"bin"
|
|
33
|
-
],
|
|
30
|
+
"files": ["dist", "bin"],
|
|
34
31
|
"engines": {
|
|
35
32
|
"node": ">=18",
|
|
36
33
|
"bun": ">=1.0"
|