kibi-cursor 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.cursor-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +78 -43
  3. package/README.md +3 -4
  4. package/agent-plugin/plugin.json +1 -1
  5. package/agent-plugin/skills/kibi-usage/SKILL.md +7 -3
  6. package/agent-plugin/skills/kibi-usage/resources/fact-lanes.md +4 -0
  7. package/agent-plugin/skills/kibi-usage/resources/operation-access.md +1 -0
  8. package/agent-plugin/skills/kibi-usage/resources/relationship-directions.md +35 -17
  9. package/agent-plugin/skills/kibi-usage/resources/source-authoring.md +36 -0
  10. package/agent-plugin/skills/kibi-usage/resources/workflows.md +52 -1
  11. package/bin/launch-kibi-mcp.mjs +8 -1
  12. package/dist/.cursor-plugin/plugin.json +2 -2
  13. package/dist/bin/launch-kibi-mcp.mjs +8 -1
  14. package/dist/guidance.d.ts +11 -0
  15. package/dist/guidance.js +39 -5
  16. package/dist/hook-runner.js +23 -2
  17. package/dist/hook-state-storage.js +3 -0
  18. package/dist/hook-state.d.ts +4 -2
  19. package/dist/hook-state.js +16 -3
  20. package/dist/kb-mcp-tools.d.ts +5 -7
  21. package/dist/kb-mcp-tools.js +6 -146
  22. package/dist/path-policy.js +17 -188
  23. package/dist/skills/.canon-hash.json +6 -6
  24. package/dist/skills/kibi-usage/SKILL.md +7 -3
  25. package/dist/skills/kibi-usage/resources/fact-lanes.md +4 -0
  26. package/dist/skills/kibi-usage/resources/operation-access.md +1 -0
  27. package/dist/skills/kibi-usage/resources/relationship-directions.md +35 -17
  28. package/dist/skills/kibi-usage/resources/source-authoring.md +36 -0
  29. package/dist/skills/kibi-usage/resources/workflows.md +52 -1
  30. package/dist/source-linked-requirements.d.ts +16 -0
  31. package/dist/source-linked-requirements.js +55 -0
  32. package/package.json +4 -1
  33. package/skills/.canon-hash.json +6 -6
  34. package/skills/kibi-usage/SKILL.md +7 -3
  35. package/skills/kibi-usage/resources/fact-lanes.md +4 -0
  36. package/skills/kibi-usage/resources/operation-access.md +1 -0
  37. package/skills/kibi-usage/resources/relationship-directions.md +35 -17
  38. package/skills/kibi-usage/resources/source-authoring.md +36 -0
  39. package/skills/kibi-usage/resources/workflows.md +52 -1
@@ -29,3 +29,39 @@ Cancellation follows the same rule: wait for the terminal journal state. A
29
29
  pre-commit rollback is retryable, a committed source with a failed derivative
30
30
  is repairable, and an indeterminate outcome is non-retryable until its recovery
31
31
  action resolves the journal.
32
+
33
+ ## Naming entities
34
+
35
+ Name every entity by what it governs, the way you would name a source file.
36
+ Never choose "the next number": parallel branches pick the same next number and
37
+ collide even when they touch unrelated behavior.
38
+
39
+ - Use `<TYPE>-<area>-<behavior>` in kebab-case, with the type prefix kept
40
+ uppercase: `REQ-cli-gc`, `SCEN-mcp-search-discovery`,
41
+ `TEST-mcp-search-discovery`, `ADR-capability-plugins-v1`,
42
+ `EVT-exact-branch-store-ensure`, and a flag named after its gate such as
43
+ `FLAG-scip-symbol-extraction`.
44
+ - The filename stem equals the frontmatter `id`
45
+ (`.kb/requirements/REQ-cli-gc.md` has `id: REQ-cli-gc`). Always set `id`
46
+ explicitly; a missing `id` falls back to a path-and-title hash that changes
47
+ when the file is renamed.
48
+ - Before creating an entity, `kb_search` the area. If an existing entity already
49
+ covers the behavior, update it, or create a replacement and link it with
50
+ `supersedes`. A `-v2` suffix is acceptable only for that direct superseding
51
+ replacement.
52
+ - A name collision with another branch usually means the same work was done
53
+ twice. Reconcile the two entities; do not rename one of them to dodge the
54
+ collision.
55
+ - The area prefix is for humans. Whether two requirements are about the same
56
+ thing is decided by their facts: a shared `subject_key` and identical
57
+ predicate or property signatures, which `domain-redundancy` and
58
+ `domain-contradictions` compare deterministically. Reuse existing subject keys
59
+ (dotted `component.aspect[.sub]`, lowercase snake segments such as
60
+ `kibi.cli.check.staged`) instead of deriving a subject from the requirement
61
+ ID.
62
+
63
+ `entity-id-style` reports new purely numeric IDs (`REQ-123`) and IDs whose
64
+ filename stem differs from `id` as non-blocking quality diagnostics. Legacy
65
+ numbered entities created before the naming standard are grandfathered and are
66
+ not renamed, because code comments such as `// implements REQ-003` still
67
+ reference them.
@@ -33,11 +33,62 @@ surfaces are a release defect: require a new package version and label any
33
33
  project override temporary. This skill never selects a package manager or
34
34
  edits dependency configuration.
35
35
 
36
+ ## Choosing a discovery mode
37
+
38
+ `kb_search` is not one behavior. It selects deterministic intent ranking when
39
+ you pass `rankingMode: "intent-v1"`, `semanticFacets`, or `sourceLocations`,
40
+ and legacy lexical ranking otherwise. Naming the tool is not enough; the inputs
41
+ decide the path.
42
+
43
+ - **Exact identity** — you already know the ID. Use `kb_query`, not search.
44
+ - **Literal token** — a symbol, file, or error string that appears verbatim.
45
+ Use lexical `kb_search` and keep the query close to the literal text.
46
+ - **Conceptual or behavioral** — "how does checkout handle a declined card".
47
+ Use intent mode and ground it in what you actually know:
48
+
49
+ ```json
50
+ {
51
+ "query": "declined card during checkout",
52
+ "rankingMode": "intent-v1",
53
+ "semanticFacets": {
54
+ "actors": ["shopper"],
55
+ "actions": ["pay"],
56
+ "objects": ["card"],
57
+ "constraints": ["declined"]
58
+ }
59
+ }
60
+ ```
61
+
62
+ - **Before changing code** — pass the file you are about to edit as a source
63
+ location, so results are matched against source-linked symbols and return
64
+ source evidence:
65
+
66
+ ```json
67
+ {
68
+ "query": "checkout total rounding",
69
+ "rankingMode": "intent-v1",
70
+ "sourceLocations": [{ "path": "src/checkout.ts", "symbol": "computeTotal" }]
71
+ }
72
+ ```
73
+
74
+ Supply facets only from the request, the code, or prior results. Inventing
75
+ facets fabricates evidence and biases ranking. Intent mode abstains below
76
+ `minScore` instead of returning weak matches; an abstention is an explicit
77
+ no-answer, so widen the query or drop to lexical search rather than treating it
78
+ as "nothing exists". There is no target percentage of intent-mode calls — a
79
+ literal identifier lookup is still best served lexically.
80
+
81
+ Results are summaries by default: identifying metadata, score, reasons, and a
82
+ snippet. Read them to pick candidates, then `kb_query` those IDs for full
83
+ bodies, or pass `fields: "full"` when you genuinely need every body. Extract
84
+ the relevant existing constraints and decisions from the response before
85
+ planning a behavior change; a search receipt is not comprehension.
86
+
36
87
  ## Discovery to Validation Sequence
37
88
 
38
89
  The canonical workflow for any KB operation follows this pattern:
39
90
 
40
- 1. **Discover**: `kb_search` with focused probes
91
+ 1. **Discover**: `kb_search` with focused probes; pick the mode above
41
92
  2. **Confirm**: `kb_query` for exact IDs and state
42
93
  3. **Inspect**: `kb_status` when freshness matters
43
94
  4. **Decompose**: `kb_semantic_advisor` on the complete normative prose; verify or supply every atomic clause
@@ -362,7 +362,14 @@ export function launchKibiMcp(
362
362
  [projectLocal.binPath, ...childArgs],
363
363
  {
364
364
  cwd: workspaceRoot,
365
- env: { ...env, KIBI_WORKSPACE: workspaceRoot },
365
+ env: {
366
+ ...env,
367
+ KIBI_WORKSPACE: workspaceRoot,
368
+ // Identify the host on usage rows. This never enables telemetry;
369
+ // KIBI_DIAGNOSTIC_MODE stays the operator's explicit opt-in and is
370
+ // passed through untouched.
371
+ KIBI_MCP_HOST: "cursor",
372
+ },
366
373
  stdio: ["inherit", "inherit", "inherit"],
367
374
  },
368
375
  );
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
3
  "displayName": "Kibi",
4
- "version": "2.0.1",
5
- "description": "Optional Cursor adapter for Kibi. Requires SWI-Prolog 9+ and project-local kibi-cli, kibi-mcp, and kibi-core before the bundled MCP server can start.",
4
+ "version": "2.1.1",
5
+ "description": "Optional Cursor adapter for Kibi. Requires project-local kibi-cli, kibi-mcp, and kibi-core (SWI-Prolog is bundled on Linux and macOS) before the bundled MCP server can start.",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "author": {
8
8
  "name": "Kibi"
@@ -362,7 +362,14 @@ export function launchKibiMcp(
362
362
  [projectLocal.binPath, ...childArgs],
363
363
  {
364
364
  cwd: workspaceRoot,
365
- env: { ...env, KIBI_WORKSPACE: workspaceRoot },
365
+ env: {
366
+ ...env,
367
+ KIBI_WORKSPACE: workspaceRoot,
368
+ // Identify the host on usage rows. This never enables telemetry;
369
+ // KIBI_DIAGNOSTIC_MODE stays the operator's explicit opt-in and is
370
+ // passed through untouched.
371
+ KIBI_MCP_HOST: "cursor",
372
+ },
366
373
  stdio: ["inherit", "inherit", "inherit"],
367
374
  },
368
375
  );
@@ -5,6 +5,17 @@ export type GuidanceContext = {
5
5
  readonly hasKibi: boolean;
6
6
  readonly mcpState: McpState;
7
7
  readonly workspaceTrusted: boolean;
8
+ /** Requirements the edited file already owns, when the manifest knows them. */
9
+ readonly linkedRequirementIds?: readonly string[];
8
10
  };
9
11
  export declare function readGuidance(filePath: string, context: GuidanceContext): string | undefined;
12
+ /**
13
+ * Guidance emitted before an edit is written.
14
+ *
15
+ * The post-edit and stop messages can only ask for repair once the code is
16
+ * already changed. This is the one point where retrieval can still inform the
17
+ * change, so it names the requirements the file already implements and asks
18
+ * for them to be read before the edit rather than reconciled after it.
19
+ */
20
+ export declare function preEditGuidance(filePath: string, context: GuidanceContext): string | undefined;
10
21
  export declare function writeGuidance(filePath: string, context: GuidanceContext): string | undefined;
package/dist/guidance.js CHANGED
@@ -18,6 +18,40 @@ export function readGuidance(filePath, context) {
18
18
  const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
19
19
  return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
20
20
  }
21
+ /**
22
+ * Guidance emitted before an edit is written.
23
+ *
24
+ * The post-edit and stop messages can only ask for repair once the code is
25
+ * already changed. This is the one point where retrieval can still inform the
26
+ * change, so it names the requirements the file already implements and asks
27
+ * for them to be read before the edit rather than reconciled after it.
28
+ */
29
+ export function preEditGuidance(filePath, context) {
30
+ if (!context.hasKibi) {
31
+ return undefined;
32
+ }
33
+ const relativePath = toRepoRelativePath(filePath, context.cwd);
34
+ if (!isMeaningfulTrackedPath(relativePath)) {
35
+ return undefined;
36
+ }
37
+ if (isDocumentationTrackedPath(relativePath)) {
38
+ return undefined;
39
+ }
40
+ const linkedRequirements = context.linkedRequirementIds ?? [];
41
+ const guidance = linkedRequirements.length > 0
42
+ ? [
43
+ `Kibi pre-edit guidance: ${relativePath} already implements ${linkedRequirements.join(", ")}.`,
44
+ `Read those requirements with kb_query before changing behavior here, and widen with kb_query({sourceFile:"${relativePath}"}) for scenarios and tests.`,
45
+ "If the change contradicts an existing requirement, resolve that in Kibi before writing the edit.",
46
+ ]
47
+ : [
48
+ `Kibi pre-edit guidance: ${relativePath} has no linked requirement in the symbol manifest.`,
49
+ `Discover existing constraints with kb_search, then kb_query({sourceFile:"${relativePath}"}), before changing behavior here.`,
50
+ "Record the requirement this change serves rather than leaving the symbol unowned.",
51
+ ];
52
+ const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
53
+ return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
54
+ }
21
55
  export function writeGuidance(filePath, context) {
22
56
  if (!context.hasKibi) {
23
57
  return undefined;
@@ -35,12 +69,12 @@ export function writeGuidance(filePath, context) {
35
69
  const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
36
70
  return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
37
71
  }
72
+ // Retrieval is asked for before the edit; this message stays scoped to the
73
+ // impact review that only becomes possible once the change exists.
38
74
  const guidance = [
39
- "Kibi write guidance: use the selected MCP or CLI JSON route to link production symbols to requirements.",
40
- `After editing, run kb_check({sourceFiles:["${relativePath}"], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) for symbol granularity and semantic review of linked requirements/tests.`,
41
- `After editing, resolve freshness with kb_search/kb_query for sourceFile="${relativePath}".`,
42
- "Prefer symbol manifest + executable_for or // implements REQ-xxx for traceability.",
43
- "Do not read or edit `.kb/` files directly. Query before mutate; run kb_upsert sequentially and kb_check before completion.",
75
+ `Kibi impact review: run kb_check({sourceFiles:["${relativePath}"], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) while this edit is fresh.`,
76
+ "Check symbol granularity and whether linked requirements, scenarios, and tests still cover the changed behavior.",
77
+ "Prefer symbol manifest + executable_for or // implements REQ-<area>-<behavior> for traceability.",
44
78
  ];
45
79
  const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
46
80
  return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
@@ -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",
@@ -86,7 +87,27 @@ export async function runHook(rawInput, environment = {}) {
86
87
  agent_message: DIRECT_KB_EDIT_WARNING,
87
88
  };
88
89
  }
89
- return emptyResult();
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 };
90
111
  }
91
112
  case "beforeReadFile": {
92
113
  const primaryPath = resolvePrimaryPath(input);
@@ -51,6 +51,7 @@ export function emptyHookState() {
51
51
  mcpState: "unknown",
52
52
  dirtyPaths: [],
53
53
  guidedReadPaths: [],
54
+ guidedPreEditPaths: [],
54
55
  guidedWritePaths: [],
55
56
  kbMutationTools: [],
56
57
  kbCheckRun: false,
@@ -164,6 +165,7 @@ function coerceHookState(value) {
164
165
  : [];
165
166
  const dirtyPaths = strings(value.dirtyPaths, normalizePath);
166
167
  const guidedReadPaths = strings(value.guidedReadPaths, normalizePath);
168
+ const guidedPreEditPaths = strings(value.guidedPreEditPaths, normalizePath);
167
169
  const guidedWritePaths = strings(value.guidedWritePaths, normalizePath);
168
170
  const kbMutationTools = strings(value.kbMutationTools, (entry) => entry.trim());
169
171
  const impactCheckedPaths = strings(value.impactCheckedPaths, normalizePath);
@@ -171,6 +173,7 @@ function coerceHookState(value) {
171
173
  mcpState: value.mcpState === "observed" ? "observed" : "unknown",
172
174
  dirtyPaths: [...new Set(dirtyPaths)].slice(-maxDirtyPaths),
173
175
  guidedReadPaths: [...new Set(guidedReadPaths)].slice(-maxGuidedPaths),
176
+ guidedPreEditPaths: [...new Set(guidedPreEditPaths)].slice(-maxGuidedPaths),
174
177
  guidedWritePaths: [...new Set(guidedWritePaths)].slice(-maxGuidedPaths),
175
178
  kbMutationTools: [...new Set(kbMutationTools)].slice(-maxKbMutationTools),
176
179
  kbCheckRun: value.kbCheckRun === true,
@@ -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: "read" | "write", guidedPath: string): HookState;
16
- export declare function hasGuidedPath(state: HookState, kind: "read" | "write", guidedPath: string): boolean;
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;
@@ -15,6 +15,14 @@ export function rememberGuidedPath(stateDir, kind, guidedPath) {
15
15
  guidedReadPaths: mergeStringPaths(state.guidedReadPaths, [normalized]),
16
16
  };
17
17
  }
18
+ if (kind === "pre-edit") {
19
+ return {
20
+ ...state,
21
+ guidedPreEditPaths: mergeStringPaths(state.guidedPreEditPaths, [
22
+ normalized,
23
+ ]),
24
+ };
25
+ }
18
26
  return {
19
27
  ...state,
20
28
  guidedWritePaths: mergeStringPaths(state.guidedWritePaths, [normalized]),
@@ -22,9 +30,14 @@ export function rememberGuidedPath(stateDir, kind, guidedPath) {
22
30
  });
23
31
  }
24
32
  export function hasGuidedPath(state, kind, guidedPath) {
25
- const normalized = normalizePath(guidedPath);
26
- const bucket = kind === "read" ? state.guidedReadPaths : state.guidedWritePaths;
27
- return bucket.includes(normalized);
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;
28
41
  }
29
42
  export function recordPlanDelivered(stateDir) {
30
43
  return updateHookState(stateDir, (state) => state.planDelivered ? state : { ...state, planDelivered: true });
@@ -1,10 +1,8 @@
1
- export type KbMcpToolCall = {
2
- toolName: string;
3
- impactCheckRun: boolean;
4
- sourceFiles: string[];
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;
@@ -1,154 +1,14 @@
1
1
  // implements REQ-cursor-kibi-plugin-v1
2
- function isRecord(value) {
3
- return typeof value === "object" && value !== null && !Array.isArray(value);
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
- if (mcpState === "observed") {
44
- return "mcp";
45
- }
46
- return workspaceTrusted ? "cli" : "setup";
7
+ return resolveSharedKibiInterface(mcpState, workspaceTrusted);
47
8
  }
48
9
  export function extractKbMcpToolCall(toolName, toolInput) {
49
- const directToolName = toolName?.trim();
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
- const toolCall = extractKbMcpToolCall(toolName, toolInput);
106
- // rationale: the fallbacks below re-derive the same name from the same
107
- // inputs, so dropping the toolCall shortcut is observationally equivalent.
108
- // Stryker disable next-line ConditionalExpression, BlockStatement
109
- if (toolCall) {
110
- return toolCall.toolName;
111
- }
112
- // rationale: any tool name that trims to a kb_ prefix already produced a
113
- // tool call above, so this branch only sees equivalent names.
114
- // Stryker disable next-line MethodExpression
115
- if (toolName) {
116
- // rationale: any name that trims to a kb_ prefix already produced a tool
117
- // call above, so this trim cannot change the returned name.
118
- // Stryker disable MethodExpression
119
- const normalized = toolName.trim();
120
- if (normalized.startsWith("kb_")) {
121
- return normalized;
122
- }
123
- // Stryker restore
124
- }
125
- if (!isRecord(toolInput)) {
126
- return undefined;
127
- }
128
- // rationale: this alias list duplicates the one inside
129
- // extractKbMcpToolCall, which already returned for every kb_ match.
130
- // Stryker disable next-line StringLiteral
131
- const directTool = readString(toolInput, ["toolName", "tool_name", "name"]);
132
- if (directTool?.startsWith("kb_")) {
133
- return directTool;
134
- }
135
- const nestedArgs = toolInput.arguments ?? toolInput.args;
136
- if (isRecord(nestedArgs)) {
137
- // rationale: this alias list duplicates the one inside
138
- // extractKbMcpToolCall, which already returned for every kb_ match.
139
- // Stryker disable next-line StringLiteral
140
- // rationale: duplicate alias list; extractKbMcpToolCall already
141
- // returned for every kb_ match.
142
- // Stryker disable StringLiteral
143
- const nestedTool = readString(nestedArgs, [
144
- "toolName",
145
- "tool_name",
146
- "name",
147
- ]);
148
- // Stryker restore
149
- if (nestedTool?.startsWith("kb_")) {
150
- return nestedTool;
151
- }
152
- }
153
- return undefined;
13
+ return extractSharedKbMcpToolName(toolName, toolInput);
154
14
  }