kibi-cursor 0.2.2 → 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 +71 -0
- package/README.md +7 -5
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/guidance.d.ts +10 -2
- package/dist/guidance.js +27 -17
- package/dist/hook-runner.d.ts +1 -0
- package/dist/hook-runner.js +41 -11
- package/dist/hook-state-storage.d.ts +11 -0
- package/dist/hook-state-storage.js +187 -0
- package/dist/hook-state.d.ts +9 -5
- package/dist/hook-state.js +14 -196
- package/dist/kb-mcp-tools.d.ts +9 -0
- package/dist/kb-mcp-tools.js +94 -0
- package/dist/messages.d.ts +4 -2
- package/dist/messages.js +33 -3
- package/dist/path-policy.d.ts +1 -0
- package/dist/path-policy.js +18 -0
- package/dist/rules/kibi-traceability.mdc +2 -0
- 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 -9
- package/dist/skills/kibi-usage/SKILL.md +123 -95
- 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-traceability.mdc +2 -0
- 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 -9
- package/skills/kibi-usage/SKILL.md +123 -95
- 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,76 @@
|
|
|
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
|
+
|
|
46
|
+
## 0.3.1
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- f1db710: Kibi check outputs now have a stable advisory diagnostics lane for auditability review signals. Operators and MCP clients can receive `qualityDiagnostics` alongside hard `violations` without advisory-only findings changing pass/fail counts or exit behavior. Existing staged impact failures, including symbol granularity violations, remain blocking. Source impact analysis now also highlights overly broad symbols, indistinguishable symbol coordinates, and mixed-purpose component/class ownership as review-only guidance.
|
|
51
|
+
|
|
52
|
+
Technical summary:
|
|
53
|
+
|
|
54
|
+
- Add the public `QualityDiagnostic` type with `error`, `warning`, `review`, and `info` severities plus explicit `blocking` semantics.
|
|
55
|
+
- Preserve existing `violations`, `diagnostics`, and `impactDiagnostics` fields while adding MCP structured `qualityDiagnostics` output support.
|
|
56
|
+
- Preserve explicitly filtered MCP `kb_check` rule semantics so advisory full-KB quality scans only run for unfiltered checks or requested impact diagnostics.
|
|
57
|
+
- Clarify Codex and Cursor bundled agent guidance so targeted checks use explicit rules and final checks omit rules for full-KB `qualityDiagnostics` review.
|
|
58
|
+
- Add quality diagnostic text formatting and shared blocking helpers that treat `blocking: true` or `severity: "error"` as hard failures.
|
|
59
|
+
- Add non-blocking `multi_requirement_symbol_review`, `duplicate_symbol_coordinate_review`, and `component_mixed_purpose_review` impact diagnostics.
|
|
60
|
+
|
|
61
|
+
## 0.3.0
|
|
62
|
+
|
|
63
|
+
### Minor Changes
|
|
64
|
+
|
|
65
|
+
- Kibi now gives agents source-impact feedback while they are still editing, instead of waiting for the commit hook to be the first signal. Meaningful source edits can be checked through MCP with changed-file impact diagnostics, so agents see coarse symbol ownership, stale symbol evidence, and semantic-review prompts while the source context is fresh. OpenCode, Cursor, and Codex adapters now steer agents toward that MCP-first workflow and keep CLI/hooks as the later safety net.
|
|
66
|
+
|
|
67
|
+
Technical summary:
|
|
68
|
+
|
|
69
|
+
- Add reusable CLI changed-file impact diagnostics and export them for MCP consumption.
|
|
70
|
+
- Extend MCP `kb_check` with source-file impact options and structured impact output.
|
|
71
|
+
- Update OpenCode, Cursor, and Codex guidance/hooks to request impact-enabled `kb_check` after source edits.
|
|
72
|
+
- Document semantic-review diagnostics and class-member granularity expectations.
|
|
73
|
+
|
|
3
74
|
## 0.2.2
|
|
4
75
|
|
|
5
76
|
### 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
|
|
|
@@ -96,22 +98,22 @@ Hooks are warning-only and never replace MCP/CLI behavior:
|
|
|
96
98
|
- **sessionStart**: bootstrap reminder when `.kb/config.json` is missing.
|
|
97
99
|
- **preToolUse**: warns on explicit direct `.kb/**` edits without blocking.
|
|
98
100
|
- **beforeReadFile** and **postToolUse (Read)**: inject source-linked lookup guidance once per path per session.
|
|
99
|
-
- **postToolUse (Write/Edit)**: inject traceability and freshness guidance once per path per session.
|
|
100
|
-
- **stop**: emits a single freshness follow-up when meaningful paths changed during the session.
|
|
101
|
+
- **postToolUse (Write/Edit)**: inject traceability and freshness guidance once per path per session, including `kb_check({sourceFiles:[...], includeImpactDiagnostics:true, includeWorkingTreeDiff:true})` for meaningful source edits.
|
|
102
|
+
- **stop**: emits a single freshness or impact-check follow-up when meaningful paths changed during the session.
|
|
101
103
|
|
|
102
104
|
### What the plugin does not do
|
|
103
105
|
|
|
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 `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,37 +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.",
|
|
40
|
+
`After editing, run kb_check({sourceFiles:["${relativePath}"], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) for symbol granularity and semantic review of linked requirements/tests.`,
|
|
34
41
|
`After editing, resolve freshness with kb_search/kb_query for sourceFile="${relativePath}".`,
|
|
35
42
|
"Prefer symbol manifest + executable_for or // implements REQ-xxx for traceability.",
|
|
36
|
-
|
|
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");
|
|
37
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
|
@@ -6,8 +6,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
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
|
-
import {
|
|
10
|
-
import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, stopFollowupMessage, } from "./messages.js";
|
|
9
|
+
import { extractKbMcpToolCall } from "./kb-mcp-tools.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
|
}
|
|
@@ -86,9 +101,12 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
86
101
|
};
|
|
87
102
|
}
|
|
88
103
|
case "postToolUse": {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
recordKbMcpTool(stateDir,
|
|
104
|
+
const kbToolCall = extractKbMcpToolCall(input.toolName, input.toolInput);
|
|
105
|
+
if (kbToolCall) {
|
|
106
|
+
recordKbMcpTool(stateDir, kbToolCall.toolName, {
|
|
107
|
+
impactCheckRun: kbToolCall.impactCheckRun,
|
|
108
|
+
sourceFiles: kbToolCall.sourceFiles,
|
|
109
|
+
});
|
|
92
110
|
}
|
|
93
111
|
const explicitPaths = extractExplicitPathFields(input.toolInput);
|
|
94
112
|
const dirtyPaths = explicitPaths
|
|
@@ -107,7 +125,12 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
107
125
|
if (hasGuidedPath(state, "read", relativePath)) {
|
|
108
126
|
return emptyResult();
|
|
109
127
|
}
|
|
110
|
-
const guidance = readGuidance(primaryPath,
|
|
128
|
+
const guidance = readGuidance(primaryPath, {
|
|
129
|
+
cwd,
|
|
130
|
+
hasKibi: kibiReady,
|
|
131
|
+
mcpState: state.mcpState,
|
|
132
|
+
workspaceTrusted,
|
|
133
|
+
});
|
|
111
134
|
if (!guidance) {
|
|
112
135
|
return emptyResult();
|
|
113
136
|
}
|
|
@@ -118,7 +141,12 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
118
141
|
if (hasGuidedPath(state, "write", relativePath)) {
|
|
119
142
|
return emptyResult();
|
|
120
143
|
}
|
|
121
|
-
const guidance = writeGuidance(primaryPath,
|
|
144
|
+
const guidance = writeGuidance(primaryPath, {
|
|
145
|
+
cwd,
|
|
146
|
+
hasKibi: kibiReady,
|
|
147
|
+
mcpState: state.mcpState,
|
|
148
|
+
workspaceTrusted,
|
|
149
|
+
});
|
|
122
150
|
if (!guidance) {
|
|
123
151
|
return emptyResult();
|
|
124
152
|
}
|
|
@@ -146,7 +174,9 @@ export async function runHook(rawInput, environment = {}) {
|
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
async function main() {
|
|
149
|
-
const result = await runHook(parseStdinJson(await readStdin())
|
|
177
|
+
const result = await runHook(parseStdinJson(await readStdin()), {
|
|
178
|
+
workspaceTrusted: process.argv.includes("--trusted-workspace"),
|
|
179
|
+
});
|
|
150
180
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
151
181
|
}
|
|
152
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,18 +1,22 @@
|
|
|
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[];
|
|
5
8
|
kbMutationTools: string[];
|
|
6
9
|
kbCheckRun: boolean;
|
|
10
|
+
impactCheckRun: boolean;
|
|
11
|
+
impactCheckedPaths: string[];
|
|
7
12
|
};
|
|
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;
|
|
12
13
|
export declare function addDirtyPaths(stateDir: string | undefined, dirtyPaths: readonly string[]): HookState;
|
|
13
14
|
export declare function rememberGuidedPath(stateDir: string | undefined, kind: "read" | "write", guidedPath: string): HookState;
|
|
14
15
|
export declare function hasGuidedPath(state: HookState, kind: "read" | "write", guidedPath: string): boolean;
|
|
15
|
-
export declare function recordKbMcpTool(stateDir: string | undefined, toolName: string
|
|
16
|
+
export declare function recordKbMcpTool(stateDir: string | undefined, toolName: string, options?: {
|
|
17
|
+
impactCheckRun?: boolean;
|
|
18
|
+
sourceFiles?: readonly string[];
|
|
19
|
+
}): HookState;
|
|
16
20
|
export declare function clearSessionHookState(stateDir: string | undefined): HookState;
|
|
17
21
|
/** @deprecated Use clearSessionHookState */
|
|
18
22
|
export declare function clearDirtyPaths(stateDir: string | undefined): HookState;
|