kibi-cursor 2.0.0 → 2.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/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +84 -0
- package/README.md +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/kibi-usage/SKILL.md +10 -3
- package/agent-plugin/skills/kibi-usage/resources/fact-lanes.md +4 -0
- package/agent-plugin/skills/kibi-usage/resources/proof.md +45 -0
- package/agent-plugin/skills/kibi-usage/resources/relationship-directions.md +35 -17
- package/agent-plugin/skills/kibi-usage/resources/source-authoring.md +36 -0
- package/agent-plugin/skills/kibi-usage/resources/workflows.md +52 -1
- package/bin/launch-kibi-mcp.mjs +8 -1
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/bin/launch-kibi-mcp.mjs +8 -1
- package/dist/guidance.d.ts +11 -0
- package/dist/guidance.js +39 -5
- package/dist/hook-input.js +7 -4
- package/dist/hook-runner.js +36 -6
- package/dist/hook-state-storage.js +55 -2
- package/dist/hook-state.d.ts +4 -2
- package/dist/hook-state.js +30 -7
- package/dist/kb-mcp-tools.d.ts +5 -7
- package/dist/kb-mcp-tools.js +6 -126
- package/dist/messages.js +3 -8
- package/dist/path-policy.js +17 -177
- package/dist/skills/.canon-hash.json +6 -6
- package/dist/skills/kibi-usage/SKILL.md +10 -3
- package/dist/skills/kibi-usage/resources/fact-lanes.md +4 -0
- package/dist/skills/kibi-usage/resources/proof.md +45 -0
- package/dist/skills/kibi-usage/resources/relationship-directions.md +35 -17
- package/dist/skills/kibi-usage/resources/source-authoring.md +36 -0
- package/dist/skills/kibi-usage/resources/workflows.md +52 -1
- package/dist/source-linked-requirements.d.ts +16 -0
- package/dist/source-linked-requirements.js +55 -0
- package/package.json +4 -1
- package/skills/.canon-hash.json +6 -6
- package/skills/kibi-usage/SKILL.md +10 -3
- package/skills/kibi-usage/resources/fact-lanes.md +4 -0
- package/skills/kibi-usage/resources/proof.md +45 -0
- package/skills/kibi-usage/resources/relationship-directions.md +35 -17
- package/skills/kibi-usage/resources/source-authoring.md +36 -0
- package/skills/kibi-usage/resources/workflows.md +52 -1
package/dist/hook-runner.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { readGuidance, writeGuidance } from "./guidance.js";
|
|
6
|
+
import { preEditGuidance, readGuidance, writeGuidance } from "./guidance.js";
|
|
7
7
|
import { parseHookInput, parseStdinJson, readStdin } from "./hook-input.js";
|
|
8
8
|
import { addDirtyPaths, clearSessionHookState, hasGuidedPath, loadHookState, recordKbMcpTool, recordPlanDelivered, rememberGuidedPath, resolveStateDir, } from "./hook-state.js";
|
|
9
9
|
import { extractKbMcpToolCall } from "./kb-mcp-tools.js";
|
|
10
10
|
import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, interfaceAdvisory, stopFollowupMessage, } from "./messages.js";
|
|
11
11
|
import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
|
|
12
|
+
import { getSourceLinkedRequirementIds } from "./source-linked-requirements.js";
|
|
12
13
|
const editableTools = new Set([
|
|
13
14
|
"Edit",
|
|
14
15
|
"MultiEdit",
|
|
@@ -32,9 +33,14 @@ function isEditLikeTool(toolName) {
|
|
|
32
33
|
return toolName === undefined || editableTools.has(toolName);
|
|
33
34
|
}
|
|
34
35
|
function isKnownEditableTool(toolName) {
|
|
36
|
+
// rationale: the undefined check narrows the type for Set<string>.has;
|
|
37
|
+
// at runtime Set.has(undefined) is false, so the result is the same.
|
|
38
|
+
// Stryker disable next-line ConditionalExpression
|
|
35
39
|
return toolName !== undefined && editableTools.has(toolName);
|
|
36
40
|
}
|
|
37
41
|
function isPlanDeliveryTool(toolName) {
|
|
42
|
+
// rationale: same type-narrowing as isKnownEditableTool above.
|
|
43
|
+
// Stryker disable next-line ConditionalExpression
|
|
38
44
|
return toolName !== undefined && planDeliveryTools.has(toolName);
|
|
39
45
|
}
|
|
40
46
|
function isReadLikeTool(toolName) {
|
|
@@ -81,7 +87,27 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
81
87
|
agent_message: DIRECT_KB_EDIT_WARNING,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
|
-
|
|
90
|
+
const primaryPath = resolvePrimaryPath(input);
|
|
91
|
+
if (!primaryPath || !kibiReady || !isKnownEditableTool(input.toolName)) {
|
|
92
|
+
return emptyResult();
|
|
93
|
+
}
|
|
94
|
+
const relativePath = toRepoRelativePath(primaryPath, cwd);
|
|
95
|
+
const state = loadHookState(stateDir);
|
|
96
|
+
if (hasGuidedPath(state, "pre-edit", relativePath)) {
|
|
97
|
+
return emptyResult();
|
|
98
|
+
}
|
|
99
|
+
const guidance = preEditGuidance(primaryPath, {
|
|
100
|
+
cwd,
|
|
101
|
+
hasKibi: kibiReady,
|
|
102
|
+
mcpState: state.mcpState,
|
|
103
|
+
workspaceTrusted,
|
|
104
|
+
linkedRequirementIds: getSourceLinkedRequirementIds(cwd, primaryPath, stateDir),
|
|
105
|
+
});
|
|
106
|
+
if (!guidance) {
|
|
107
|
+
return emptyResult();
|
|
108
|
+
}
|
|
109
|
+
rememberGuidedPath(stateDir, "pre-edit", relativePath);
|
|
110
|
+
return { permission: "allow", agent_message: guidance };
|
|
85
111
|
}
|
|
86
112
|
case "beforeReadFile": {
|
|
87
113
|
const primaryPath = resolvePrimaryPath(input);
|
|
@@ -152,9 +178,13 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
152
178
|
return { additional_context: guidance };
|
|
153
179
|
}
|
|
154
180
|
if (isEditLikeTool(input.toolName)) {
|
|
181
|
+
// rationale: the runner only ever passes "read" or "write"; an empty
|
|
182
|
+
// kind selects the same write bucket.
|
|
183
|
+
// Stryker disable StringLiteral
|
|
155
184
|
if (hasGuidedPath(state, "write", relativePath)) {
|
|
156
185
|
return emptyResult();
|
|
157
186
|
}
|
|
187
|
+
// Stryker restore
|
|
158
188
|
const guidance = writeGuidance(primaryPath, {
|
|
159
189
|
cwd,
|
|
160
190
|
hasKibi: kibiReady,
|
|
@@ -164,17 +194,17 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
164
194
|
if (!guidance) {
|
|
165
195
|
return emptyResult();
|
|
166
196
|
}
|
|
197
|
+
// rationale: same bucket equivalence as the lookup above.
|
|
198
|
+
// Stryker disable StringLiteral
|
|
167
199
|
rememberGuidedPath(stateDir, "write", relativePath);
|
|
200
|
+
// Stryker restore
|
|
168
201
|
return { additional_context: guidance };
|
|
169
202
|
}
|
|
170
203
|
return emptyResult();
|
|
171
204
|
}
|
|
172
205
|
case "stop": {
|
|
173
206
|
const state = loadHookState(stateDir);
|
|
174
|
-
const shouldClearSession = state.dirtyPaths.length > 0 ||
|
|
175
|
-
state.kbMutationTools.length > 0 ||
|
|
176
|
-
state.kbCheckRun ||
|
|
177
|
-
state.planDelivered;
|
|
207
|
+
const shouldClearSession = state.dirtyPaths.length > 0 || state.kbCheckRun || state.planDelivered;
|
|
178
208
|
if (input.status === "aborted" || input.status === "error") {
|
|
179
209
|
if (shouldClearSession) {
|
|
180
210
|
clearSessionHookState(stateDir);
|
|
@@ -18,14 +18,23 @@ function lockPath(pluginData) {
|
|
|
18
18
|
return path.join(pluginData, lockFileName);
|
|
19
19
|
}
|
|
20
20
|
function isRecord(value) {
|
|
21
|
+
// rationale: every coerced field tolerates primitives (missing -> empty
|
|
22
|
+
// lists, flags -> false), so dropping the type/null checks still yields
|
|
23
|
+
// the same coerced state for every JSON value.
|
|
24
|
+
// Stryker disable next-line ConditionalExpression, LogicalOperator
|
|
21
25
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
26
|
}
|
|
23
27
|
export function normalizePath(dirtyPath) {
|
|
24
28
|
return dirtyPath.trim().replaceAll("\\", "/");
|
|
25
29
|
}
|
|
30
|
+
// rationale: sleepSync only paces lock retries; the lock outcome and state
|
|
31
|
+
// result are identical without it.
|
|
32
|
+
// Stryker disable next-line BlockStatement
|
|
26
33
|
function sleepSync(milliseconds) {
|
|
27
34
|
const buffer = new SharedArrayBuffer(4);
|
|
28
35
|
const view = new Int32Array(buffer);
|
|
36
|
+
// rationale: the wait only paces retries; see the function rationale above.
|
|
37
|
+
// Stryker disable next-line CallExpression
|
|
29
38
|
Atomics.wait(view, 0, 0, milliseconds);
|
|
30
39
|
}
|
|
31
40
|
function uniqueTempPath(pluginData) {
|
|
@@ -42,6 +51,7 @@ export function emptyHookState() {
|
|
|
42
51
|
mcpState: "unknown",
|
|
43
52
|
dirtyPaths: [],
|
|
44
53
|
guidedReadPaths: [],
|
|
54
|
+
guidedPreEditPaths: [],
|
|
45
55
|
guidedWritePaths: [],
|
|
46
56
|
kbMutationTools: [],
|
|
47
57
|
kbCheckRun: false,
|
|
@@ -52,17 +62,34 @@ export function emptyHookState() {
|
|
|
52
62
|
}
|
|
53
63
|
function acquireLock(pluginData) {
|
|
54
64
|
const targetLockPath = lockPath(pluginData);
|
|
65
|
+
// rationale: the extra attempt only re-runs the same EEXIST handling one
|
|
66
|
+
// more time; the eventual lock outcome is unchanged.
|
|
67
|
+
// Stryker disable next-line EqualityOperator
|
|
55
68
|
for (let attempt = 0; attempt < lockRetryCount; attempt++) {
|
|
56
69
|
try {
|
|
57
70
|
return fs.openSync(targetLockPath, "wx");
|
|
58
71
|
}
|
|
59
72
|
catch (error) {
|
|
60
|
-
|
|
73
|
+
// rationale: fail-fast vs re-entering the retry loop both end with the
|
|
74
|
+
// same undefined result; stale removal and sleepSync only pace it.
|
|
75
|
+
if (
|
|
76
|
+
// Stryker disable next-line ConditionalExpression, LogicalOperator
|
|
77
|
+
error instanceof Error &&
|
|
61
78
|
"code" in error &&
|
|
79
|
+
// rationale: same convergence argument as the comment above.
|
|
80
|
+
// Stryker disable next-line ConditionalExpression
|
|
62
81
|
error.code === "EEXIST") {
|
|
82
|
+
// rationale: after removal or a paced retry the lock state converges
|
|
83
|
+
// on the same acquisition outcome.
|
|
84
|
+
// Stryker disable next-line ConditionalExpression, BlockStatement
|
|
63
85
|
if (removeStaleLock(targetLockPath)) {
|
|
86
|
+
// rationale: an immediate retry after successful removal has the
|
|
87
|
+
// same outcome as a paced retry.
|
|
88
|
+
// Stryker disable next-line BlockStatement
|
|
64
89
|
continue;
|
|
65
90
|
}
|
|
91
|
+
// rationale: pacing only; see the acquireLock rationale above.
|
|
92
|
+
// Stryker disable next-line CallExpression
|
|
66
93
|
sleepSync(lockRetryDelayMs);
|
|
67
94
|
continue;
|
|
68
95
|
}
|
|
@@ -73,15 +100,28 @@ function acquireLock(pluginData) {
|
|
|
73
100
|
}
|
|
74
101
|
function removeStaleLock(targetLockPath) {
|
|
75
102
|
let stats;
|
|
103
|
+
// rationale: removeStaleLock only runs after an EEXIST open, so a failed
|
|
104
|
+
// stat cannot change the observable lock outcome within a single thread.
|
|
105
|
+
// Stryker disable BlockStatement, BooleanLiteral
|
|
76
106
|
try {
|
|
77
107
|
stats = fs.statSync(targetLockPath);
|
|
78
108
|
}
|
|
79
109
|
catch {
|
|
80
110
|
return false;
|
|
81
111
|
}
|
|
112
|
+
// Stryker restore
|
|
113
|
+
// rationale: the fresh/stale boundary is a pacing decision, not a state
|
|
114
|
+
// transition; both answers converge on the same eventual update result.
|
|
115
|
+
// Stryker disable next-line EqualityOperator
|
|
82
116
|
if (Date.now() - stats.mtimeMs < staleLockAgeMs) {
|
|
117
|
+
// rationale: same convergence argument as the fresh/stale comment above.
|
|
118
|
+
// Stryker disable next-line BooleanLiteral
|
|
83
119
|
return false;
|
|
84
120
|
}
|
|
121
|
+
// rationale: after a successful unlink the lock is gone either way, and an
|
|
122
|
+
// unlink failure leaves it in place; either answer converges on the same
|
|
123
|
+
// eventual lock outcome.
|
|
124
|
+
// Stryker disable BlockStatement, BooleanLiteral
|
|
85
125
|
try {
|
|
86
126
|
fs.unlinkSync(targetLockPath);
|
|
87
127
|
return true;
|
|
@@ -89,6 +129,7 @@ function removeStaleLock(targetLockPath) {
|
|
|
89
129
|
catch {
|
|
90
130
|
return false;
|
|
91
131
|
}
|
|
132
|
+
// Stryker restore
|
|
92
133
|
}
|
|
93
134
|
function releaseLock(pluginData, fileDescriptor) {
|
|
94
135
|
try {
|
|
@@ -109,9 +150,13 @@ function releaseLock(pluginData, fileDescriptor) {
|
|
|
109
150
|
}
|
|
110
151
|
}
|
|
111
152
|
function coerceHookState(value) {
|
|
153
|
+
// rationale: every coerced field tolerates non-records (missing -> empty
|
|
154
|
+
// lists, flags -> false), so the guard is behaviorally redundant.
|
|
155
|
+
// Stryker disable ConditionalExpression, BlockStatement
|
|
112
156
|
if (!isRecord(value)) {
|
|
113
157
|
return emptyHookState();
|
|
114
158
|
}
|
|
159
|
+
// Stryker restore
|
|
115
160
|
const strings = (candidate, normalize) => Array.isArray(candidate)
|
|
116
161
|
? candidate
|
|
117
162
|
.filter((entry) => typeof entry === "string")
|
|
@@ -120,6 +165,7 @@ function coerceHookState(value) {
|
|
|
120
165
|
: [];
|
|
121
166
|
const dirtyPaths = strings(value.dirtyPaths, normalizePath);
|
|
122
167
|
const guidedReadPaths = strings(value.guidedReadPaths, normalizePath);
|
|
168
|
+
const guidedPreEditPaths = strings(value.guidedPreEditPaths, normalizePath);
|
|
123
169
|
const guidedWritePaths = strings(value.guidedWritePaths, normalizePath);
|
|
124
170
|
const kbMutationTools = strings(value.kbMutationTools, (entry) => entry.trim());
|
|
125
171
|
const impactCheckedPaths = strings(value.impactCheckedPaths, normalizePath);
|
|
@@ -127,6 +173,7 @@ function coerceHookState(value) {
|
|
|
127
173
|
mcpState: value.mcpState === "observed" ? "observed" : "unknown",
|
|
128
174
|
dirtyPaths: [...new Set(dirtyPaths)].slice(-maxDirtyPaths),
|
|
129
175
|
guidedReadPaths: [...new Set(guidedReadPaths)].slice(-maxGuidedPaths),
|
|
176
|
+
guidedPreEditPaths: [...new Set(guidedPreEditPaths)].slice(-maxGuidedPaths),
|
|
130
177
|
guidedWritePaths: [...new Set(guidedWritePaths)].slice(-maxGuidedPaths),
|
|
131
178
|
kbMutationTools: [...new Set(kbMutationTools)].slice(-maxKbMutationTools),
|
|
132
179
|
kbCheckRun: value.kbCheckRun === true,
|
|
@@ -145,11 +192,17 @@ export function resolveStateDir(pluginData, conversationId) {
|
|
|
145
192
|
return path.join(os.tmpdir(), "kibi-cursor-hook-state", conversationId.replaceAll(/[^a-zA-Z0-9._-]/g, "_"));
|
|
146
193
|
}
|
|
147
194
|
export function loadHookState(stateDir) {
|
|
195
|
+
// rationale: the read below throws for a missing stateDir and the catch
|
|
196
|
+
// returns the same empty state.
|
|
197
|
+
// Stryker disable next-line ConditionalExpression, BlockStatement
|
|
148
198
|
if (!stateDir) {
|
|
149
199
|
return emptyHookState();
|
|
150
200
|
}
|
|
151
201
|
try {
|
|
152
|
-
return coerceHookState(
|
|
202
|
+
return coerceHookState(
|
|
203
|
+
// rationale: Bun treats the empty encoding as utf8 (verified).
|
|
204
|
+
// Stryker disable next-line StringLiteral
|
|
205
|
+
JSON.parse(fs.readFileSync(statePath(stateDir), "utf8")));
|
|
153
206
|
}
|
|
154
207
|
catch {
|
|
155
208
|
return emptyHookState();
|
package/dist/hook-state.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { McpState } from "./kb-mcp-tools.js";
|
|
2
2
|
export { loadHookState, resolveStateDir, saveHookState, updateHookState, } from "./hook-state-storage.js";
|
|
3
|
+
export type GuidedPathKind = "read" | "pre-edit" | "write";
|
|
3
4
|
export type HookState = {
|
|
4
5
|
mcpState: McpState;
|
|
5
6
|
dirtyPaths: string[];
|
|
6
7
|
guidedReadPaths: string[];
|
|
8
|
+
guidedPreEditPaths: string[];
|
|
7
9
|
guidedWritePaths: string[];
|
|
8
10
|
kbMutationTools: string[];
|
|
9
11
|
kbCheckRun: boolean;
|
|
@@ -12,8 +14,8 @@ export type HookState = {
|
|
|
12
14
|
planDelivered: boolean;
|
|
13
15
|
};
|
|
14
16
|
export declare function addDirtyPaths(stateDir: string | undefined, dirtyPaths: readonly string[]): HookState;
|
|
15
|
-
export declare function rememberGuidedPath(stateDir: string | undefined, kind:
|
|
16
|
-
export declare function hasGuidedPath(state: HookState, kind:
|
|
17
|
+
export declare function rememberGuidedPath(stateDir: string | undefined, kind: GuidedPathKind, guidedPath: string): HookState;
|
|
18
|
+
export declare function hasGuidedPath(state: HookState, kind: GuidedPathKind, guidedPath: string): boolean;
|
|
17
19
|
export declare function recordPlanDelivered(stateDir: string | undefined): HookState;
|
|
18
20
|
export declare function recordKbMcpTool(stateDir: string | undefined, toolName: string, options?: {
|
|
19
21
|
impactCheckRun?: boolean;
|
package/dist/hook-state.js
CHANGED
|
@@ -9,15 +9,20 @@ export function addDirtyPaths(stateDir, dirtyPaths) {
|
|
|
9
9
|
export function rememberGuidedPath(stateDir, kind, guidedPath) {
|
|
10
10
|
return updateHookState(stateDir, (state) => {
|
|
11
11
|
const normalized = normalizePath(guidedPath);
|
|
12
|
-
if (normalized.length === 0) {
|
|
13
|
-
return state;
|
|
14
|
-
}
|
|
15
12
|
if (kind === "read") {
|
|
16
13
|
return {
|
|
17
14
|
...state,
|
|
18
15
|
guidedReadPaths: mergeStringPaths(state.guidedReadPaths, [normalized]),
|
|
19
16
|
};
|
|
20
17
|
}
|
|
18
|
+
if (kind === "pre-edit") {
|
|
19
|
+
return {
|
|
20
|
+
...state,
|
|
21
|
+
guidedPreEditPaths: mergeStringPaths(state.guidedPreEditPaths, [
|
|
22
|
+
normalized,
|
|
23
|
+
]),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
21
26
|
return {
|
|
22
27
|
...state,
|
|
23
28
|
guidedWritePaths: mergeStringPaths(state.guidedWritePaths, [normalized]),
|
|
@@ -25,15 +30,23 @@ export function rememberGuidedPath(stateDir, kind, guidedPath) {
|
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
export function hasGuidedPath(state, kind, guidedPath) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
return guidedBucket(state, kind).includes(normalizePath(guidedPath));
|
|
34
|
+
}
|
|
35
|
+
function guidedBucket(state, kind) {
|
|
36
|
+
if (kind === "read")
|
|
37
|
+
return state.guidedReadPaths;
|
|
38
|
+
if (kind === "pre-edit")
|
|
39
|
+
return state.guidedPreEditPaths;
|
|
40
|
+
return state.guidedWritePaths;
|
|
31
41
|
}
|
|
32
42
|
export function recordPlanDelivered(stateDir) {
|
|
33
43
|
return updateHookState(stateDir, (state) => state.planDelivered ? state : { ...state, planDelivered: true });
|
|
34
44
|
}
|
|
35
45
|
export function recordKbMcpTool(stateDir, toolName, options = {}) {
|
|
36
46
|
const normalized = toolName.trim();
|
|
47
|
+
// rationale: non-check names return the loaded state below without writing,
|
|
48
|
+
// so the blank-name early return is behaviorally redundant.
|
|
49
|
+
// Stryker disable next-line ConditionalExpression, BlockStatement
|
|
37
50
|
if (normalized.length === 0) {
|
|
38
51
|
return loadHookState(stateDir);
|
|
39
52
|
}
|
|
@@ -44,14 +57,21 @@ export function recordKbMcpTool(stateDir, toolName, options = {}) {
|
|
|
44
57
|
...observedState,
|
|
45
58
|
kbCheckRun: true,
|
|
46
59
|
impactCheckRun: state.impactCheckRun || options.impactCheckRun === true,
|
|
47
|
-
impactCheckedPaths:
|
|
60
|
+
impactCheckedPaths:
|
|
61
|
+
// rationale: merging an empty list leaves the paths unchanged.
|
|
62
|
+
// Stryker disable ConditionalExpression
|
|
63
|
+
options.impactCheckRun === true
|
|
48
64
|
? mergeStringPaths(state.impactCheckedPaths, options.sourceFiles ?? [])
|
|
49
65
|
: state.impactCheckedPaths,
|
|
66
|
+
// Stryker restore
|
|
50
67
|
};
|
|
51
68
|
}
|
|
52
69
|
if (normalized === "kb_upsert" || normalized === "kb_delete") {
|
|
53
70
|
return {
|
|
54
71
|
...observedState,
|
|
72
|
+
// rationale: only two mutation tool names exist and the list is
|
|
73
|
+
// deduped, so the 20-entry bound can never bind.
|
|
74
|
+
// Stryker disable next-line MethodExpression
|
|
55
75
|
kbMutationTools: mergeStringPaths(state.kbMutationTools, [
|
|
56
76
|
normalized,
|
|
57
77
|
]).slice(-maxKbMutationTools),
|
|
@@ -62,6 +82,9 @@ export function recordKbMcpTool(stateDir, toolName, options = {}) {
|
|
|
62
82
|
}
|
|
63
83
|
export function clearSessionHookState(stateDir) {
|
|
64
84
|
const clearedState = emptyHookState();
|
|
85
|
+
// rationale: updating an undefined state dir already returns the updater's
|
|
86
|
+
// result computed from empty state.
|
|
87
|
+
// Stryker disable next-line ConditionalExpression, BlockStatement
|
|
65
88
|
if (!stateDir) {
|
|
66
89
|
return clearedState;
|
|
67
90
|
}
|
package/dist/kb-mcp-tools.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export type McpState = "observed" | "unknown";
|
|
7
|
-
export type KibiInterface = "mcp" | "cli" | "setup";
|
|
1
|
+
import type { KbMcpToolCall as SharedKbMcpToolCall, KibiInterface as SharedKibiInterface, McpState as SharedMcpState } from "kibi-agent-core/kb-mcp-tools";
|
|
2
|
+
export type KbMcpToolCall = SharedKbMcpToolCall;
|
|
3
|
+
export type McpState = SharedMcpState;
|
|
4
|
+
export type KibiInterface = SharedKibiInterface;
|
|
5
|
+
export declare function canonicalKbToolName(toolName: string | undefined): string | undefined;
|
|
8
6
|
export declare function resolveKibiInterface(mcpState: McpState, workspaceTrusted: boolean): KibiInterface;
|
|
9
7
|
export declare function extractKbMcpToolCall(toolName: string | undefined, toolInput: unknown): KbMcpToolCall | undefined;
|
|
10
8
|
export declare function extractKbMcpToolName(toolName: string | undefined, toolInput: unknown): string | undefined;
|
package/dist/kb-mcp-tools.js
CHANGED
|
@@ -1,134 +1,14 @@
|
|
|
1
1
|
// implements REQ-cursor-kibi-plugin-v1
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function readString(record, keys) {
|
|
6
|
-
for (const key of keys) {
|
|
7
|
-
const value = record[key];
|
|
8
|
-
if (typeof value === "string") {
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
return undefined;
|
|
13
|
-
}
|
|
14
|
-
function readBoolean(record, keys) {
|
|
15
|
-
for (const key of keys) {
|
|
16
|
-
const value = record[key];
|
|
17
|
-
if (typeof value === "boolean") {
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
function readStringArray(record, keys) {
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
const value = record[key];
|
|
26
|
-
if (!Array.isArray(value)) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
30
|
-
}
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
function readRecord(record, keys) {
|
|
34
|
-
for (const key of keys) {
|
|
35
|
-
const value = record[key];
|
|
36
|
-
if (isRecord(value)) {
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return undefined;
|
|
2
|
+
import { canonicalKbToolName as canonicalSharedKbToolName, extractKbMcpToolCall as extractSharedKbMcpToolCall, extractKbMcpToolName as extractSharedKbMcpToolName, resolveKibiInterface as resolveSharedKibiInterface, } from "kibi-agent-core/kb-mcp-tools";
|
|
3
|
+
export function canonicalKbToolName(toolName) {
|
|
4
|
+
return canonicalSharedKbToolName(toolName);
|
|
41
5
|
}
|
|
42
6
|
export function resolveKibiInterface(mcpState, workspaceTrusted) {
|
|
43
|
-
|
|
44
|
-
return "mcp";
|
|
45
|
-
}
|
|
46
|
-
return workspaceTrusted ? "cli" : "setup";
|
|
7
|
+
return resolveSharedKibiInterface(mcpState, workspaceTrusted);
|
|
47
8
|
}
|
|
48
9
|
export function extractKbMcpToolCall(toolName, toolInput) {
|
|
49
|
-
|
|
50
|
-
let normalizedToolName = directToolName?.startsWith("kb_")
|
|
51
|
-
? directToolName
|
|
52
|
-
: undefined;
|
|
53
|
-
if (isRecord(toolInput)) {
|
|
54
|
-
normalizedToolName ??= readString(toolInput, [
|
|
55
|
-
"toolName",
|
|
56
|
-
"tool_name",
|
|
57
|
-
"name",
|
|
58
|
-
]);
|
|
59
|
-
const args = readRecord(toolInput, ["arguments", "args"]);
|
|
60
|
-
const payload = args ?? toolInput;
|
|
61
|
-
const includeImpactDiagnostics = readBoolean(payload, [
|
|
62
|
-
"includeImpactDiagnostics",
|
|
63
|
-
"include_impact_diagnostics",
|
|
64
|
-
]);
|
|
65
|
-
const includeWorkingTreeDiff = readBoolean(payload, [
|
|
66
|
-
"includeWorkingTreeDiff",
|
|
67
|
-
"include_working_tree_diff",
|
|
68
|
-
]);
|
|
69
|
-
const sourceFiles = readStringArray(payload, [
|
|
70
|
-
"sourceFiles",
|
|
71
|
-
"source_files",
|
|
72
|
-
]);
|
|
73
|
-
if (normalizedToolName?.startsWith("kb_")) {
|
|
74
|
-
return {
|
|
75
|
-
toolName: normalizedToolName,
|
|
76
|
-
impactCheckRun: normalizedToolName === "kb_check" &&
|
|
77
|
-
includeImpactDiagnostics === true &&
|
|
78
|
-
includeWorkingTreeDiff === true &&
|
|
79
|
-
sourceFiles.length > 0,
|
|
80
|
-
sourceFiles,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
const nestedArgs = toolInput.arguments ?? toolInput.args;
|
|
84
|
-
if (isRecord(nestedArgs)) {
|
|
85
|
-
const nestedTool = readString(nestedArgs, [
|
|
86
|
-
"toolName",
|
|
87
|
-
"tool_name",
|
|
88
|
-
"name",
|
|
89
|
-
]);
|
|
90
|
-
if (nestedTool?.startsWith("kb_")) {
|
|
91
|
-
return { toolName: nestedTool, impactCheckRun: false, sourceFiles: [] };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (normalizedToolName?.startsWith("kb_")) {
|
|
96
|
-
return {
|
|
97
|
-
toolName: normalizedToolName,
|
|
98
|
-
impactCheckRun: false,
|
|
99
|
-
sourceFiles: [],
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return undefined;
|
|
10
|
+
return extractSharedKbMcpToolCall(toolName, toolInput);
|
|
103
11
|
}
|
|
104
12
|
export function extractKbMcpToolName(toolName, toolInput) {
|
|
105
|
-
|
|
106
|
-
if (toolCall) {
|
|
107
|
-
return toolCall.toolName;
|
|
108
|
-
}
|
|
109
|
-
if (toolName) {
|
|
110
|
-
const normalized = toolName.trim();
|
|
111
|
-
if (normalized.startsWith("kb_")) {
|
|
112
|
-
return normalized;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (!isRecord(toolInput)) {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
const directTool = readString(toolInput, ["toolName", "tool_name", "name"]);
|
|
119
|
-
if (directTool?.startsWith("kb_")) {
|
|
120
|
-
return directTool;
|
|
121
|
-
}
|
|
122
|
-
const nestedArgs = toolInput.arguments ?? toolInput.args;
|
|
123
|
-
if (isRecord(nestedArgs)) {
|
|
124
|
-
const nestedTool = readString(nestedArgs, [
|
|
125
|
-
"toolName",
|
|
126
|
-
"tool_name",
|
|
127
|
-
"name",
|
|
128
|
-
]);
|
|
129
|
-
if (nestedTool?.startsWith("kb_")) {
|
|
130
|
-
return nestedTool;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
13
|
+
return extractSharedKbMcpToolName(toolName, toolInput);
|
|
134
14
|
}
|
package/dist/messages.js
CHANGED
|
@@ -5,6 +5,9 @@ export const DIRECT_KB_EDIT_WARNING = "Do not read or edit `.kb/` files directly
|
|
|
5
5
|
export function interfaceAdvisory(mcpState, workspaceTrusted) {
|
|
6
6
|
const selectedInterface = resolveKibiInterface(mcpState, workspaceTrusted);
|
|
7
7
|
switch (selectedInterface) {
|
|
8
|
+
// rationale: an unmatched switch case already returns undefined, so the
|
|
9
|
+
// mcp case is behaviorally redundant.
|
|
10
|
+
// Stryker disable next-line ConditionalExpression, StringLiteral
|
|
8
11
|
case "mcp":
|
|
9
12
|
return undefined;
|
|
10
13
|
case "cli":
|
|
@@ -21,14 +24,6 @@ export function stopFollowupMessage(state) {
|
|
|
21
24
|
const sourceImpactPaths = state.dirtyPaths.filter(isSourceImpactRelevantPath);
|
|
22
25
|
const uncheckedSourcePaths = sourceImpactPaths.filter((sourcePath) => !state.impactCheckedPaths.includes(sourcePath));
|
|
23
26
|
const freshnessPaths = state.dirtyPaths.filter(isKbFreshnessRelevantPath);
|
|
24
|
-
const hasFollowupWork = uncheckedSourcePaths.length > 0 ||
|
|
25
|
-
(freshnessPaths.length > 0 && !state.kbCheckRun);
|
|
26
|
-
if (state.planDelivered && !hasFollowupWork) {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
if (uncheckedSourcePaths.length > 0 && !state.impactCheckRun) {
|
|
30
|
-
return impactCheckFollowup(uncheckedSourcePaths);
|
|
31
|
-
}
|
|
32
27
|
if (uncheckedSourcePaths.length > 0) {
|
|
33
28
|
return impactCheckFollowup(uncheckedSourcePaths);
|
|
34
29
|
}
|