kibi-cursor 0.3.1 → 0.5.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.
Files changed (37) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +5 -3
  4. package/dist/.cursor-plugin/plugin.json +1 -1
  5. package/dist/guidance.d.ts +10 -2
  6. package/dist/guidance.js +26 -17
  7. package/dist/hook-runner.d.ts +1 -0
  8. package/dist/hook-runner.js +34 -7
  9. package/dist/hook-state-storage.d.ts +11 -0
  10. package/dist/hook-state-storage.js +187 -0
  11. package/dist/hook-state.d.ts +3 -4
  12. package/dist/hook-state.js +6 -206
  13. package/dist/kb-mcp-tools.d.ts +3 -0
  14. package/dist/kb-mcp-tools.js +6 -0
  15. package/dist/messages.d.ts +4 -2
  16. package/dist/messages.js +14 -2
  17. package/dist/rules/kibi-workflow.mdc +3 -3
  18. package/dist/skills/.canon-hash.json +10 -0
  19. package/dist/skills/init-kibi/SKILL.md +50 -11
  20. package/dist/skills/kibi-freshness/SKILL.md +32 -10
  21. package/dist/skills/kibi-traceability/SKILL.md +41 -11
  22. package/dist/skills/kibi-usage/SKILL.md +123 -97
  23. package/dist/skills/kibi-usage/resources/fact-lanes.md +225 -0
  24. package/dist/skills/kibi-usage/resources/operation-access.md +44 -0
  25. package/dist/skills/kibi-usage/resources/relationship-directions.md +119 -0
  26. package/dist/skills/kibi-usage/resources/workflows.md +65 -0
  27. package/package.json +2 -2
  28. package/rules/kibi-workflow.mdc +3 -3
  29. package/skills/.canon-hash.json +10 -0
  30. package/skills/init-kibi/SKILL.md +50 -11
  31. package/skills/kibi-freshness/SKILL.md +32 -10
  32. package/skills/kibi-traceability/SKILL.md +41 -11
  33. package/skills/kibi-usage/SKILL.md +123 -97
  34. package/skills/kibi-usage/resources/fact-lanes.md +225 -0
  35. package/skills/kibi-usage/resources/operation-access.md +44 -0
  36. package/skills/kibi-usage/resources/relationship-directions.md +119 -0
  37. package/skills/kibi-usage/resources/workflows.md +65 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
3
  "displayName": "Kibi",
4
- "version": "0.3.1",
4
+ "version": "0.5.0",
5
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.",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "author": {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # kibi-cursor
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b2b1792: Kibi guidance now helps agents distinguish suitable relational predicates from scalar constraints and review-only claims without replacing readable requirements. CLI, Codex, and Cursor users receive the same predicate-first decision tree and authoritative examples, reducing invented predicates and unsafe modeling.
8
+
9
+ - Add built-in, project-local, deny, strict-scalar, ambiguity, false-positive, and ontology-gap guidance to the canonical `kibi-usage` skill.
10
+ - Regenerate the Codex and Cursor mirrors with matching canonical hashes.
11
+
12
+ ### Patch Changes
13
+
14
+ - 610b5be: The improved Kibi guidance skills will ship to CLI, Codex, and Cursor users in the next package release. This keeps the canonical CLI skill bundle and the generated client-plugin mirrors aligned for downstream installs.
15
+
16
+ - Release the canonical skills bundled by `kibi-cli`.
17
+ - Release the generated `kibi-codex` and `kibi-cursor` skill mirrors.
18
+
19
+ ## 0.4.0
20
+
21
+ ### Minor Changes
22
+
23
+ - a0fee4a: The Cursor plugin now supports capability-based Kibi interface selection, using visible MCP tools first and a trusted project-local CLI fallback when needed. Deterministic worktree resolution keeps both interface choices anchored to the intended repository.
24
+
25
+ - Add MCP-first, CLI-fallback agent guidance.
26
+ - Resolve trusted worktree runtime paths deterministically.
27
+
28
+ ### Patch Changes
29
+
30
+ - cafa25f: Agents can now select Kibi by available capability instead of stopping at MCP-specific guidance. The bundled skills prefer approved MCP tools, fall back safely to a project-local non-installing CLI runner, and provide executable JSON recipes plus an exact 18-operation access catalog.
31
+
32
+ - Document every shared MCP operation's dedicated CLI route, input mode, effects, Prolog requirement, mutability, and telemetry handling.
33
+ - Regenerate Cursor and Codex skill mirrors from the canonical capability-based source.
34
+
35
+ - 23e815a: Agents can now keep using Kibi when MCP tools are unavailable but a trusted project-local CLI is ready. Guidance across Cursor, OpenCode, and MCP documentation now selects the interface by capability and stops for operator action only when neither safe surface is available.
36
+
37
+ - Replace MCP-exclusive guidance with the visible-MCP, trusted-CLI JSON route, and blocked state machine.
38
+ - Preserve direct `.kb/` access prohibitions, discovery-before-mutation, sequential writes, and completion validation gates.
39
+
40
+ - e71f1ce: Cursor dogfood sessions now keep each linked worktree as the Kibi data workspace while launching a compatible built MCP runtime from that worktree or its primary checkout. Invalid, stale, or unrelated builds are rejected without installing packages, and Cursor hooks offer the project-local CLI only as advisory guidance after explicit workspace trust.
41
+
42
+ - Add deterministic build, runtime, SWI-Prolog, and package-version checks to the Cursor worktree resolver.
43
+ - Preserve an explicit `KIBI_WORKSPACE` when the MCP diagnostic launcher starts from another runtime root.
44
+ - Track MCP capability as `observed` or `unknown` and keep hook-driven CLI fallback non-executing.
45
+
3
46
  ## 0.3.1
4
47
 
5
48
  ### Patch Changes
package/README.md CHANGED
@@ -25,6 +25,8 @@ Full setup: [docs/install.md](https://github.com/Looted/kibi/blob/develop/docs/i
25
25
 
26
26
  Add this plugin only after the base packages work in your project.
27
27
 
28
+ **Git worktrees:** the published plugin MCP entry (`npx --no-install kibi-mcp`) needs `kibi-mcp` resolvable from that worktree's `node_modules` (run install there). For this monorepo's dogfood path, prefer project `.cursor/mcp.json`, which can fall back to the primary checkout's built MCP — see [DEV.md](./DEV.md#linked-worktrees).
29
+
28
30
  ## Installation
29
31
 
30
32
  ### Cursor Marketplace (recommended)
@@ -85,7 +87,7 @@ Manual MCP fallback (no plugin install required):
85
87
 
86
88
  ### Discovery-first rules and skills
87
89
 
88
- - **Rules**: always-on MCP workflow guidance plus optional traceability rules for source files.
90
+ - **Rules**: always-on capability-based workflow guidance plus optional traceability rules for source files.
89
91
  - **Skills**: `kibi-usage`, `init-kibi`, `kibi-freshness`, and `kibi-traceability`.
90
92
  - **Commands**: `/init-kibi` documents the `kb_autopilot_generate` bootstrap workflow.
91
93
 
@@ -104,14 +106,14 @@ Hooks are warning-only and never replace MCP/CLI behavior:
104
106
  Per the thin-adapter architecture, `kibi-cursor` does not:
105
107
 
106
108
  - Own KB storage, parsing, or validation
107
- - Run background `kibi sync` or CLI `kibi check` (use git hooks and MCP tools; for source edits, use impact-enabled MCP `kb_check` first)
109
+ - Run background `kibi sync` or CLI `kibi check` from hooks (agents select visible MCP tools or trusted project-local CLI JSON routes; for source edits, use impact-enabled `kb_check` first)
108
110
  - Block agent actions in advisory mode
109
111
 
110
112
  ## Architecture
111
113
 
112
114
  `kibi-cursor` is a thin bridge layer:
113
115
 
114
- - **Agent-visible guidance**: public MCP tools (`kb_search`, `kb_query`, `kb_upsert`, impact-enabled `kb_check`, etc.) and the `/init-kibi` command
116
+ - **Agent-visible guidance**: visible public MCP tools or trusted project-local CLI JSON routes (`--input`), plus a blocked/operator state when neither is available
115
117
  - **Editor-specific value**: Cursor hooks for read/write reminders and session freshness follow-ups
116
118
  - **Foundation**: `kibi-core`, `kibi-cli`, and `kibi-mcp` remain required for project-local operations
117
119
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
3
  "displayName": "Kibi",
4
- "version": "0.3.1",
4
+ "version": "0.5.0",
5
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.",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "author": {
@@ -1,2 +1,10 @@
1
- export declare function readGuidance(filePath: string, cwd: string | undefined, hasKibi: boolean): string | undefined;
2
- export declare function writeGuidance(filePath: string, cwd: string | undefined, hasKibi: boolean): string | undefined;
1
+ import type { McpState } from "./kb-mcp-tools.js";
2
+ export { resolveKibiInterface } from "./kb-mcp-tools.js";
3
+ export type GuidanceContext = {
4
+ readonly cwd: string | undefined;
5
+ readonly hasKibi: boolean;
6
+ readonly mcpState: McpState;
7
+ readonly workspaceTrusted: boolean;
8
+ };
9
+ export declare function readGuidance(filePath: string, context: GuidanceContext): string | undefined;
10
+ export declare function writeGuidance(filePath: string, context: GuidanceContext): string | undefined;
package/dist/guidance.js CHANGED
@@ -1,38 +1,47 @@
1
- // implements REQ-cursor-kibi-plugin-v1
1
+ import { interfaceAdvisory } from "./messages.js";
2
+ // implements REQ-cursor-kibi-plugin-v1, REQ-agent-kibi-interface-selection
2
3
  import { isDocumentationTrackedPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
3
- export function readGuidance(filePath, cwd, hasKibi) {
4
- if (!hasKibi) {
4
+ export { resolveKibiInterface } from "./kb-mcp-tools.js";
5
+ export function readGuidance(filePath, context) {
6
+ if (!context.hasKibi) {
5
7
  return undefined;
6
8
  }
7
- const relativePath = toRepoRelativePath(filePath, cwd);
9
+ const relativePath = toRepoRelativePath(filePath, context.cwd);
8
10
  if (!isMeaningfulTrackedPath(relativePath)) {
9
11
  return undefined;
10
12
  }
11
- return [
12
- "Kibi read guidance: before changing this file, discover linked knowledge with Kibi MCP.",
13
+ const guidance = [
14
+ "Kibi read guidance: before changing this file, discover linked knowledge through the selected MCP or CLI JSON route.",
13
15
  `Start with kb_search, then kb_query with sourceFile="${relativePath}".`,
14
16
  "Prefer Kibi facts and requirements over long inline comments for durable knowledge.",
15
- ].join("\n");
17
+ ];
18
+ const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
19
+ return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
16
20
  }
17
- export function writeGuidance(filePath, cwd, hasKibi) {
18
- if (!hasKibi) {
21
+ export function writeGuidance(filePath, context) {
22
+ if (!context.hasKibi) {
19
23
  return undefined;
20
24
  }
21
- const relativePath = toRepoRelativePath(filePath, cwd);
25
+ const relativePath = toRepoRelativePath(filePath, context.cwd);
22
26
  if (!isMeaningfulTrackedPath(relativePath)) {
23
27
  return undefined;
24
28
  }
25
29
  if (isDocumentationTrackedPath(relativePath)) {
26
- return [
30
+ const guidance = [
27
31
  "Kibi write guidance: keep REQ, SCEN, and TEST artifacts separate.",
28
- "Update KB entities through kb_upsert and finish with kb_check.",
29
- "Do not edit .kb/ files directly.",
30
- ].join("\n");
32
+ "Query before mutate. Update KB entities through kb_upsert sequentially. Run kb_check before completion.",
33
+ "Do not read or edit `.kb/` files directly.",
34
+ ];
35
+ const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
36
+ return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
31
37
  }
32
- return [
33
- "Kibi write guidance: link production symbols to requirements.",
38
+ const guidance = [
39
+ "Kibi write guidance: use the selected MCP or CLI JSON route to link production symbols to requirements.",
34
40
  `After editing, run kb_check({sourceFiles:["${relativePath}"], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) for symbol granularity and semantic review of linked requirements/tests.`,
35
41
  `After editing, resolve freshness with kb_search/kb_query for sourceFile="${relativePath}".`,
36
42
  "Prefer symbol manifest + executable_for or // implements REQ-xxx for traceability.",
37
- ].join("\n");
43
+ "Do not read or edit `.kb/` files directly. Query before mutate; run kb_upsert sequentially and kb_check before completion.",
44
+ ];
45
+ const advisory = interfaceAdvisory(context.mcpState, context.workspaceTrusted);
46
+ return advisory ? [advisory, ...guidance].join("\n") : guidance.join("\n");
38
47
  }
@@ -8,6 +8,7 @@ export type CursorHookResult = {
8
8
  };
9
9
  export type HookEnvironment = {
10
10
  pluginData?: string;
11
+ workspaceTrusted?: boolean;
11
12
  };
12
13
  export declare function runHook(rawInput: unknown, environment?: HookEnvironment): Promise<CursorHookResult>;
13
14
  export declare const hookRunnerPath: string;
@@ -7,7 +7,7 @@ import { readGuidance, writeGuidance } from "./guidance.js";
7
7
  import { parseHookInput, parseStdinJson, readStdin } from "./hook-input.js";
8
8
  import { addDirtyPaths, clearSessionHookState, hasGuidedPath, loadHookState, recordKbMcpTool, rememberGuidedPath, resolveStateDir, } from "./hook-state.js";
9
9
  import { extractKbMcpToolCall } from "./kb-mcp-tools.js";
10
- import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, stopFollowupMessage, } from "./messages.js";
10
+ import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, interfaceAdvisory, stopFollowupMessage, } from "./messages.js";
11
11
  import { extractExplicitPathFields, isDirectKbPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
12
12
  const editableTools = new Set([
13
13
  "Edit",
@@ -48,12 +48,22 @@ export async function runHook(rawInput, environment = {}) {
48
48
  const stateDir = resolveStateDir(pluginData, input.conversationId);
49
49
  const cwd = resolveWorkspaceRoot(input);
50
50
  const kibiReady = hasKibiConfig(cwd);
51
+ const workspaceTrusted = environment.workspaceTrusted === true;
52
+ const initialState = loadHookState(stateDir);
51
53
  switch (input.event) {
52
54
  case "sessionStart":
53
55
  if (!kibiReady) {
54
- return { additional_context: BOOTSTRAP_REMINDER };
56
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
57
+ return {
58
+ additional_context: advisory
59
+ ? `${BOOTSTRAP_REMINDER}\n${advisory}`
60
+ : BOOTSTRAP_REMINDER,
61
+ };
62
+ }
63
+ {
64
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
65
+ return advisory ? { additional_context: advisory } : emptyResult();
55
66
  }
56
- return emptyResult();
57
67
  case "preToolUse": {
58
68
  const explicitPaths = extractExplicitPathFields(input.toolInput);
59
69
  const hasDirectKbEdit = isEditLikeTool(input.toolName) && explicitPaths.some(isDirectKbPath);
@@ -75,7 +85,12 @@ export async function runHook(rawInput, environment = {}) {
75
85
  if (hasGuidedPath(state, "read", relativePath)) {
76
86
  return { permission: "allow" };
77
87
  }
78
- const guidance = readGuidance(primaryPath, cwd, kibiReady);
88
+ const guidance = readGuidance(primaryPath, {
89
+ cwd,
90
+ hasKibi: kibiReady,
91
+ mcpState: state.mcpState,
92
+ workspaceTrusted,
93
+ });
79
94
  if (!guidance) {
80
95
  return { permission: "allow" };
81
96
  }
@@ -110,7 +125,12 @@ export async function runHook(rawInput, environment = {}) {
110
125
  if (hasGuidedPath(state, "read", relativePath)) {
111
126
  return emptyResult();
112
127
  }
113
- const guidance = readGuidance(primaryPath, cwd, kibiReady);
128
+ const guidance = readGuidance(primaryPath, {
129
+ cwd,
130
+ hasKibi: kibiReady,
131
+ mcpState: state.mcpState,
132
+ workspaceTrusted,
133
+ });
114
134
  if (!guidance) {
115
135
  return emptyResult();
116
136
  }
@@ -121,7 +141,12 @@ export async function runHook(rawInput, environment = {}) {
121
141
  if (hasGuidedPath(state, "write", relativePath)) {
122
142
  return emptyResult();
123
143
  }
124
- const guidance = writeGuidance(primaryPath, cwd, kibiReady);
144
+ const guidance = writeGuidance(primaryPath, {
145
+ cwd,
146
+ hasKibi: kibiReady,
147
+ mcpState: state.mcpState,
148
+ workspaceTrusted,
149
+ });
125
150
  if (!guidance) {
126
151
  return emptyResult();
127
152
  }
@@ -149,7 +174,9 @@ export async function runHook(rawInput, environment = {}) {
149
174
  }
150
175
  }
151
176
  async function main() {
152
- const result = await runHook(parseStdinJson(await readStdin()));
177
+ const result = await runHook(parseStdinJson(await readStdin()), {
178
+ workspaceTrusted: process.argv.includes("--trusted-workspace"),
179
+ });
153
180
  process.stdout.write(`${JSON.stringify(result)}\n`);
154
181
  }
155
182
  const invokedPath = process.argv[1]
@@ -0,0 +1,11 @@
1
+ import type { HookState } from "./hook-state.js";
2
+ export declare const maxDirtyPaths = 50;
3
+ export declare const maxGuidedPaths = 100;
4
+ export declare const maxKbMutationTools = 20;
5
+ export declare function normalizePath(dirtyPath: string): string;
6
+ export declare function mergeStringPaths(existingPaths: readonly string[], nextPaths: readonly string[]): string[];
7
+ export declare function emptyHookState(): HookState;
8
+ export declare function resolveStateDir(pluginData: string | undefined, conversationId: string | undefined): string | undefined;
9
+ export declare function loadHookState(stateDir: string | undefined): HookState;
10
+ export declare function saveHookState(stateDir: string | undefined, state: HookState): void;
11
+ export declare function updateHookState(stateDir: string | undefined, updater: (state: HookState) => HookState): HookState;
@@ -0,0 +1,187 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const stateFileName = "hook-state.json";
6
+ const lockFileName = "hook-state.lock";
7
+ export const maxDirtyPaths = 50;
8
+ export const maxGuidedPaths = 100;
9
+ export const maxKbMutationTools = 20;
10
+ const lockRetryCount = 100;
11
+ const lockRetryDelayMs = 5;
12
+ const staleLockAgeMs = 30_000;
13
+ function statePath(pluginData) {
14
+ return path.join(pluginData, stateFileName);
15
+ }
16
+ function lockPath(pluginData) {
17
+ return path.join(pluginData, lockFileName);
18
+ }
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ export function normalizePath(dirtyPath) {
23
+ return dirtyPath.trim().replaceAll("\\", "/");
24
+ }
25
+ function sleepSync(milliseconds) {
26
+ const buffer = new SharedArrayBuffer(4);
27
+ const view = new Int32Array(buffer);
28
+ Atomics.wait(view, 0, 0, milliseconds);
29
+ }
30
+ function uniqueTempPath(pluginData) {
31
+ return path.join(pluginData, `${stateFileName}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
32
+ }
33
+ export function mergeStringPaths(existingPaths, nextPaths) {
34
+ const merged = [...existingPaths, ...nextPaths]
35
+ .map(normalizePath)
36
+ .filter((dirtyPath) => dirtyPath.length > 0);
37
+ return [...new Set(merged)].slice(-maxGuidedPaths);
38
+ }
39
+ export function emptyHookState() {
40
+ return {
41
+ mcpState: "unknown",
42
+ dirtyPaths: [],
43
+ guidedReadPaths: [],
44
+ guidedWritePaths: [],
45
+ kbMutationTools: [],
46
+ kbCheckRun: false,
47
+ impactCheckRun: false,
48
+ impactCheckedPaths: [],
49
+ };
50
+ }
51
+ function acquireLock(pluginData) {
52
+ const targetLockPath = lockPath(pluginData);
53
+ for (let attempt = 0; attempt < lockRetryCount; attempt++) {
54
+ try {
55
+ return fs.openSync(targetLockPath, "wx");
56
+ }
57
+ catch (error) {
58
+ if (error instanceof Error &&
59
+ "code" in error &&
60
+ error.code === "EEXIST") {
61
+ if (removeStaleLock(targetLockPath)) {
62
+ continue;
63
+ }
64
+ sleepSync(lockRetryDelayMs);
65
+ continue;
66
+ }
67
+ return undefined;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ function removeStaleLock(targetLockPath) {
73
+ let stats;
74
+ try {
75
+ stats = fs.statSync(targetLockPath);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ if (Date.now() - stats.mtimeMs < staleLockAgeMs) {
81
+ return false;
82
+ }
83
+ try {
84
+ fs.unlinkSync(targetLockPath);
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ function releaseLock(pluginData, fileDescriptor) {
92
+ try {
93
+ fs.closeSync(fileDescriptor);
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof Error)) {
97
+ throw error;
98
+ }
99
+ }
100
+ try {
101
+ fs.unlinkSync(lockPath(pluginData));
102
+ }
103
+ catch (error) {
104
+ if (!(error instanceof Error)) {
105
+ throw error;
106
+ }
107
+ }
108
+ }
109
+ function coerceHookState(value) {
110
+ if (!isRecord(value)) {
111
+ return emptyHookState();
112
+ }
113
+ const strings = (candidate, normalize) => Array.isArray(candidate)
114
+ ? candidate
115
+ .filter((entry) => typeof entry === "string")
116
+ .map(normalize)
117
+ .filter((entry) => entry.length > 0)
118
+ : [];
119
+ const dirtyPaths = strings(value.dirtyPaths, normalizePath);
120
+ const guidedReadPaths = strings(value.guidedReadPaths, normalizePath);
121
+ const guidedWritePaths = strings(value.guidedWritePaths, normalizePath);
122
+ const kbMutationTools = strings(value.kbMutationTools, (entry) => entry.trim());
123
+ const impactCheckedPaths = strings(value.impactCheckedPaths, normalizePath);
124
+ return {
125
+ mcpState: value.mcpState === "observed" ? "observed" : "unknown",
126
+ dirtyPaths: [...new Set(dirtyPaths)].slice(-maxDirtyPaths),
127
+ guidedReadPaths: [...new Set(guidedReadPaths)].slice(-maxGuidedPaths),
128
+ guidedWritePaths: [...new Set(guidedWritePaths)].slice(-maxGuidedPaths),
129
+ kbMutationTools: [...new Set(kbMutationTools)].slice(-maxKbMutationTools),
130
+ kbCheckRun: value.kbCheckRun === true,
131
+ impactCheckRun: value.impactCheckRun === true,
132
+ impactCheckedPaths: [...new Set(impactCheckedPaths)].slice(-maxGuidedPaths),
133
+ };
134
+ }
135
+ export function resolveStateDir(pluginData, conversationId) {
136
+ if (pluginData) {
137
+ return pluginData;
138
+ }
139
+ if (!conversationId) {
140
+ return undefined;
141
+ }
142
+ return path.join(os.tmpdir(), "kibi-cursor-hook-state", conversationId.replaceAll(/[^a-zA-Z0-9._-]/g, "_"));
143
+ }
144
+ export function loadHookState(stateDir) {
145
+ if (!stateDir) {
146
+ return emptyHookState();
147
+ }
148
+ try {
149
+ return coerceHookState(JSON.parse(fs.readFileSync(statePath(stateDir), "utf8")));
150
+ }
151
+ catch {
152
+ return emptyHookState();
153
+ }
154
+ }
155
+ export function saveHookState(stateDir, state) {
156
+ if (!stateDir) {
157
+ return;
158
+ }
159
+ fs.mkdirSync(stateDir, { recursive: true });
160
+ const boundedState = coerceHookState(state);
161
+ const tempPath = uniqueTempPath(stateDir);
162
+ fs.writeFileSync(tempPath, `${JSON.stringify(boundedState)}\n`);
163
+ fs.renameSync(tempPath, statePath(stateDir));
164
+ }
165
+ export function updateHookState(stateDir, updater) {
166
+ const initialState = loadHookState(stateDir);
167
+ if (!stateDir) {
168
+ return updater(initialState);
169
+ }
170
+ fs.mkdirSync(stateDir, { recursive: true });
171
+ const lockFileDescriptor = acquireLock(stateDir);
172
+ if (lockFileDescriptor === undefined) {
173
+ return updater(initialState);
174
+ }
175
+ try {
176
+ const lockedState = loadHookState(stateDir);
177
+ const nextState = updater(lockedState);
178
+ saveHookState(stateDir, nextState);
179
+ return nextState;
180
+ }
181
+ catch {
182
+ return updater(initialState);
183
+ }
184
+ finally {
185
+ releaseLock(stateDir, lockFileDescriptor);
186
+ }
187
+ }
@@ -1,4 +1,7 @@
1
+ import type { McpState } from "./kb-mcp-tools.js";
2
+ export { loadHookState, resolveStateDir, saveHookState, updateHookState, } from "./hook-state-storage.js";
1
3
  export type HookState = {
4
+ mcpState: McpState;
2
5
  dirtyPaths: string[];
3
6
  guidedReadPaths: string[];
4
7
  guidedWritePaths: string[];
@@ -7,10 +10,6 @@ export type HookState = {
7
10
  impactCheckRun: boolean;
8
11
  impactCheckedPaths: string[];
9
12
  };
10
- export declare function resolveStateDir(pluginData: string | undefined, conversationId: string | undefined): string | undefined;
11
- export declare function loadHookState(stateDir: string | undefined): HookState;
12
- export declare function saveHookState(stateDir: string | undefined, state: HookState): void;
13
- export declare function updateHookState(stateDir: string | undefined, updater: (state: HookState) => HookState): HookState;
14
13
  export declare function addDirtyPaths(stateDir: string | undefined, dirtyPaths: readonly string[]): HookState;
15
14
  export declare function rememberGuidedPath(stateDir: string | undefined, kind: "read" | "write", guidedPath: string): HookState;
16
15
  export declare function hasGuidedPath(state: HookState, kind: "read" | "write", guidedPath: string): boolean;