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.
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +43 -0
- package/README.md +5 -3
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/guidance.d.ts +10 -2
- package/dist/guidance.js +26 -17
- package/dist/hook-runner.d.ts +1 -0
- package/dist/hook-runner.js +34 -7
- package/dist/hook-state-storage.d.ts +11 -0
- package/dist/hook-state-storage.js +187 -0
- package/dist/hook-state.d.ts +3 -4
- package/dist/hook-state.js +6 -206
- package/dist/kb-mcp-tools.d.ts +3 -0
- package/dist/kb-mcp-tools.js +6 -0
- package/dist/messages.d.ts +4 -2
- package/dist/messages.js +14 -2
- package/dist/rules/kibi-workflow.mdc +3 -3
- package/dist/skills/.canon-hash.json +10 -0
- package/dist/skills/init-kibi/SKILL.md +50 -11
- package/dist/skills/kibi-freshness/SKILL.md +32 -10
- package/dist/skills/kibi-traceability/SKILL.md +41 -11
- package/dist/skills/kibi-usage/SKILL.md +123 -97
- package/dist/skills/kibi-usage/resources/fact-lanes.md +225 -0
- package/dist/skills/kibi-usage/resources/operation-access.md +44 -0
- package/dist/skills/kibi-usage/resources/relationship-directions.md +119 -0
- package/dist/skills/kibi-usage/resources/workflows.md +65 -0
- package/package.json +2 -2
- package/rules/kibi-workflow.mdc +3 -3
- package/skills/.canon-hash.json +10 -0
- package/skills/init-kibi/SKILL.md +50 -11
- package/skills/kibi-freshness/SKILL.md +32 -10
- package/skills/kibi-traceability/SKILL.md +41 -11
- package/skills/kibi-usage/SKILL.md +123 -97
- package/skills/kibi-usage/resources/fact-lanes.md +225 -0
- package/skills/kibi-usage/resources/operation-access.md +44 -0
- package/skills/kibi-usage/resources/relationship-directions.md +119 -0
- 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.
|
|
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
|
|
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` (
|
|
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 (`
|
|
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.
|
|
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/dist/guidance.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
12
|
-
"Kibi read guidance: before changing this file, discover linked knowledge
|
|
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
|
-
]
|
|
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,
|
|
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
|
-
|
|
30
|
+
const guidance = [
|
|
27
31
|
"Kibi write guidance: keep REQ, SCEN, and TEST artifacts separate.",
|
|
28
|
-
"Update KB entities through kb_upsert
|
|
29
|
-
"Do not edit
|
|
30
|
-
]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/hook-runner.d.ts
CHANGED
|
@@ -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;
|
package/dist/hook-runner.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
+
}
|
package/dist/hook-state.d.ts
CHANGED
|
@@ -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;
|