kibi-cursor 0.6.0 → 1.0.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 (92) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +171 -0
  3. package/README.md +13 -10
  4. package/agent-plugin/plugin.json +1 -1
  5. package/agent-plugin/skills/kibi-bootstrap/SKILL.md +58 -0
  6. package/agent-plugin/skills/kibi-bootstrap/resources/bootstrap.md +11 -0
  7. package/agent-plugin/skills/kibi-bootstrap/resources/branch-lifecycle.md +7 -0
  8. package/agent-plugin/skills/kibi-bootstrap/resources/operation-access.md +5 -0
  9. package/agent-plugin/skills/kibi-bootstrap/resources/source-authoring.md +8 -0
  10. package/agent-plugin/skills/kibi-freshness/SKILL.md +40 -10
  11. package/agent-plugin/skills/kibi-freshness/resources/branch-lifecycle.md +7 -0
  12. package/agent-plugin/skills/kibi-freshness/resources/operation-access.md +6 -0
  13. package/agent-plugin/skills/kibi-freshness/resources/source-authoring.md +7 -0
  14. package/agent-plugin/skills/kibi-traceability/SKILL.md +47 -47
  15. package/agent-plugin/skills/kibi-traceability/resources/branch-lifecycle.md +7 -0
  16. package/agent-plugin/skills/kibi-traceability/resources/operation-access.md +5 -0
  17. package/agent-plugin/skills/kibi-traceability/resources/source-authoring.md +6 -0
  18. package/agent-plugin/skills/kibi-traceability/resources/traceability.md +8 -0
  19. package/agent-plugin/skills/kibi-usage/SKILL.md +115 -239
  20. package/agent-plugin/skills/kibi-usage/resources/branch-lifecycle.md +31 -0
  21. package/agent-plugin/skills/kibi-usage/resources/kb-improvement.md +38 -0
  22. package/agent-plugin/skills/kibi-usage/resources/operation-access.md +36 -38
  23. package/agent-plugin/skills/kibi-usage/resources/relationship-directions.md +3 -3
  24. package/agent-plugin/skills/kibi-usage/resources/source-authoring.md +31 -0
  25. package/agent-plugin/skills/kibi-usage/resources/workflows.md +56 -3
  26. package/bin/launch-kibi-mcp.d.mts +18 -0
  27. package/bin/launch-kibi-mcp.mjs +378 -0
  28. package/commands/kibi-bootstrap.md +19 -0
  29. package/dist/.cursor-plugin/plugin.json +1 -1
  30. package/dist/bin/launch-kibi-mcp.d.mts +18 -0
  31. package/dist/bin/launch-kibi-mcp.mjs +378 -0
  32. package/dist/commands/kibi-bootstrap.md +19 -0
  33. package/dist/hook-input.d.ts +2 -0
  34. package/dist/hook-input.js +10 -0
  35. package/dist/hook-runner.js +34 -12
  36. package/dist/hook-state-storage.js +3 -0
  37. package/dist/hook-state.d.ts +2 -0
  38. package/dist/hook-state.js +3 -0
  39. package/dist/mcp.json +2 -2
  40. package/dist/messages.js +6 -1
  41. package/dist/path-policy.js +43 -1
  42. package/dist/skills/.canon-hash.json +21 -7
  43. package/dist/skills/kibi-bootstrap/SKILL.md +58 -0
  44. package/dist/skills/kibi-bootstrap/resources/bootstrap.md +11 -0
  45. package/dist/skills/kibi-bootstrap/resources/branch-lifecycle.md +7 -0
  46. package/dist/skills/kibi-bootstrap/resources/operation-access.md +5 -0
  47. package/dist/skills/kibi-bootstrap/resources/source-authoring.md +8 -0
  48. package/dist/skills/kibi-freshness/SKILL.md +40 -10
  49. package/dist/skills/kibi-freshness/resources/branch-lifecycle.md +7 -0
  50. package/dist/skills/kibi-freshness/resources/operation-access.md +6 -0
  51. package/dist/skills/kibi-freshness/resources/source-authoring.md +7 -0
  52. package/dist/skills/kibi-traceability/SKILL.md +47 -47
  53. package/dist/skills/kibi-traceability/resources/branch-lifecycle.md +7 -0
  54. package/dist/skills/kibi-traceability/resources/operation-access.md +5 -0
  55. package/dist/skills/kibi-traceability/resources/source-authoring.md +6 -0
  56. package/dist/skills/kibi-traceability/resources/traceability.md +8 -0
  57. package/dist/skills/kibi-usage/SKILL.md +115 -239
  58. package/dist/skills/kibi-usage/resources/branch-lifecycle.md +31 -0
  59. package/dist/skills/kibi-usage/resources/kb-improvement.md +38 -0
  60. package/dist/skills/kibi-usage/resources/operation-access.md +36 -38
  61. package/dist/skills/kibi-usage/resources/relationship-directions.md +3 -3
  62. package/dist/skills/kibi-usage/resources/source-authoring.md +31 -0
  63. package/dist/skills/kibi-usage/resources/workflows.md +56 -3
  64. package/mcp.json +2 -2
  65. package/package.json +4 -3
  66. package/skills/.canon-hash.json +21 -7
  67. package/skills/kibi-bootstrap/SKILL.md +58 -0
  68. package/skills/kibi-bootstrap/resources/bootstrap.md +11 -0
  69. package/skills/kibi-bootstrap/resources/branch-lifecycle.md +7 -0
  70. package/skills/kibi-bootstrap/resources/operation-access.md +5 -0
  71. package/skills/kibi-bootstrap/resources/source-authoring.md +8 -0
  72. package/skills/kibi-freshness/SKILL.md +40 -10
  73. package/skills/kibi-freshness/resources/branch-lifecycle.md +7 -0
  74. package/skills/kibi-freshness/resources/operation-access.md +6 -0
  75. package/skills/kibi-freshness/resources/source-authoring.md +7 -0
  76. package/skills/kibi-traceability/SKILL.md +47 -47
  77. package/skills/kibi-traceability/resources/branch-lifecycle.md +7 -0
  78. package/skills/kibi-traceability/resources/operation-access.md +5 -0
  79. package/skills/kibi-traceability/resources/source-authoring.md +6 -0
  80. package/skills/kibi-traceability/resources/traceability.md +8 -0
  81. package/skills/kibi-usage/SKILL.md +115 -239
  82. package/skills/kibi-usage/resources/branch-lifecycle.md +31 -0
  83. package/skills/kibi-usage/resources/kb-improvement.md +38 -0
  84. package/skills/kibi-usage/resources/operation-access.md +36 -38
  85. package/skills/kibi-usage/resources/relationship-directions.md +3 -3
  86. package/skills/kibi-usage/resources/source-authoring.md +31 -0
  87. package/skills/kibi-usage/resources/workflows.md +56 -3
  88. package/agent-plugin/skills/init-kibi/SKILL.md +0 -62
  89. package/commands/init-kibi.md +0 -21
  90. package/dist/commands/init-kibi.md +0 -21
  91. package/dist/skills/init-kibi/SKILL.md +0 -62
  92. package/skills/init-kibi/SKILL.md +0 -62
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Consumer-side entrypoint for Cursor's MCP process launcher.
5
+ */
6
+
7
+ /**
8
+ * Launch the kibi-mcp package installed by the consumer project.
9
+ *
10
+ * Cursor starts plugin MCP commands with the plugin directory as their cwd.
11
+ * This adapter deliberately knows nothing about the Kibi source tree: it only
12
+ * resolves a consumer workspace, finds that workspace's kibi-mcp package, and
13
+ * runs its declared bin with the consumer as both cwd and KIBI_WORKSPACE.
14
+ */
15
+
16
+ import { spawn } from "node:child_process";
17
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
18
+ import { createRequire } from "node:module";
19
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const PACKAGE_NAME = "kibi-mcp";
23
+ const PLACEHOLDER_PATTERN = /\$\{[^}]+\}/;
24
+ const SIGNAL_EXIT_CODES = {
25
+ SIGHUP: 129,
26
+ SIGINT: 130,
27
+ SIGTERM: 143,
28
+ SIGBREAK: 148,
29
+ };
30
+
31
+ function readJson(path) {
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ }
34
+
35
+ function isDirectory(path) {
36
+ try {
37
+ return statSync(path).isDirectory();
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ function isPlaceholder(value) {
44
+ return PLACEHOLDER_PATTERN.test(value.trim());
45
+ }
46
+
47
+ function normalizeWorkspacePath(value, baseDirectory) {
48
+ const trimmed = typeof value === "string" ? value.trim() : "";
49
+ if (!trimmed || isPlaceholder(trimmed)) return null;
50
+ const candidate = isAbsolute(trimmed)
51
+ ? trimmed
52
+ : resolve(baseDirectory, trimmed);
53
+ return isDirectory(candidate) ? resolve(candidate) : null;
54
+ }
55
+
56
+ /**
57
+ * Cursor has used both a platform-delimited string and a JSON-like list for
58
+ * workspace-folder environment values. Accept both without treating a
59
+ * literal unresolved placeholder as a path relative to the plugin directory.
60
+ */
61
+ export function parseWorkspaceFolderPaths(value) {
62
+ if (typeof value !== "string" || !value.trim()) return [];
63
+ const trimmed = value.trim();
64
+ if (trimmed.startsWith("[")) {
65
+ try {
66
+ const parsed = JSON.parse(trimmed);
67
+ if (Array.isArray(parsed)) {
68
+ return parsed.filter((item) => typeof item === "string");
69
+ }
70
+ } catch {
71
+ // Fall through to the platform-delimited form.
72
+ }
73
+ }
74
+
75
+ const separator = process.platform === "win32" ? ";" : ":";
76
+ const separated = trimmed.split(separator);
77
+ const values =
78
+ separated.length === 1 && trimmed.includes(",")
79
+ ? trimmed.split(",")
80
+ : separated;
81
+ return values
82
+ .flatMap((item) =>
83
+ process.platform === "win32" ? item.split(",") : [item],
84
+ )
85
+ .map((item) => item.trim())
86
+ .filter(Boolean);
87
+ }
88
+
89
+ function packageJsonForResolvedFile(startPath) {
90
+ let current = resolve(startPath);
91
+ try {
92
+ if (!statSync(current).isDirectory()) current = dirname(current);
93
+ } catch {
94
+ current = dirname(current);
95
+ }
96
+
97
+ while (true) {
98
+ const packageJsonPath = join(current, "package.json");
99
+ if (existsSync(packageJsonPath)) {
100
+ try {
101
+ const packageJson = readJson(packageJsonPath);
102
+ if (packageJson.name === PACKAGE_NAME) {
103
+ return { packageJsonPath, packageRoot: current, packageJson };
104
+ }
105
+ } catch {
106
+ // Keep walking if a parent package manifest is malformed.
107
+ }
108
+ }
109
+ const parent = dirname(current);
110
+ if (parent === current) return null;
111
+ current = parent;
112
+ }
113
+ }
114
+
115
+ function isWithinRoot(rootPath, candidatePath) {
116
+ const relativePath = relative(resolve(rootPath), resolve(candidatePath));
117
+ return (
118
+ relativePath === "" ||
119
+ (!relativePath.startsWith("..") && !isAbsolute(relativePath))
120
+ );
121
+ }
122
+
123
+ function hasConsumerNodeModulesLink(workspaceRoot, packageRoot) {
124
+ try {
125
+ const linkPath = join(workspaceRoot, "node_modules", PACKAGE_NAME);
126
+ const linkedRoot = realpathSync(linkPath);
127
+ return (
128
+ isWithinRoot(linkedRoot, packageRoot) ||
129
+ isWithinRoot(packageRoot, linkedRoot)
130
+ );
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ function isProjectScopedPackage(workspaceRoot, packageRoot) {
137
+ if (isWithinRoot(workspaceRoot, packageRoot)) return true;
138
+
139
+ // pnpm can expose a package through a symlink whose realpath is outside the
140
+ // workspace. Require that the consumer's node_modules entry points to it;
141
+ // this rejects arbitrary NODE_PATH/global packages while retaining that
142
+ // package-manager layout. Yarn PnP has no node_modules entry, so only its
143
+ // active resolver hook may authorize an out-of-tree package.
144
+ return (
145
+ hasConsumerNodeModulesLink(workspaceRoot, packageRoot) ||
146
+ Boolean(process.versions.pnp)
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Resolve the consumer's package through Node's project-scoped resolver. This
152
+ * works with npm, pnpm, Yarn, and Bun layouts, unlike a hard-coded .bin path.
153
+ */
154
+ export function resolveProjectLocalMcp(workspaceRoot) {
155
+ const root = resolve(workspaceRoot);
156
+ const consumerRequire = createRequire(join(root, "package.json"));
157
+ let packageInfo;
158
+
159
+ try {
160
+ packageInfo = packageJsonForResolvedFile(
161
+ consumerRequire.resolve(`${PACKAGE_NAME}/package.json`),
162
+ );
163
+ } catch {
164
+ try {
165
+ packageInfo = packageJsonForResolvedFile(
166
+ consumerRequire.resolve(PACKAGE_NAME),
167
+ );
168
+ } catch {
169
+ packageInfo = null;
170
+ }
171
+ }
172
+
173
+ if (!packageInfo) {
174
+ throw new Error(
175
+ `[KIBI-CURSOR] No project-local ${PACKAGE_NAME} package was found for ${root}. ` +
176
+ `Install it in that workspace (for example: npm install --save-dev ${PACKAGE_NAME}) and reload Cursor.`,
177
+ );
178
+ }
179
+
180
+ if (!isProjectScopedPackage(root, packageInfo.packageRoot)) {
181
+ throw new Error(
182
+ `[KIBI-CURSOR] Resolved ${PACKAGE_NAME} outside the consumer workspace: ${packageInfo.packageRoot}. Install it in ${root}; global, plugin-local, and ambient NODE_PATH packages are not supported.`,
183
+ );
184
+ }
185
+
186
+ const declaredBin = packageInfo.packageJson.bin;
187
+ const binEntry =
188
+ typeof declaredBin === "string"
189
+ ? declaredBin
190
+ : declaredBin && typeof declaredBin === "object"
191
+ ? declaredBin[PACKAGE_NAME]
192
+ : undefined;
193
+ if (typeof binEntry !== "string" || !binEntry) {
194
+ throw new Error(
195
+ `[KIBI-CURSOR] Project-local ${PACKAGE_NAME} does not declare a ${PACKAGE_NAME} executable.`,
196
+ );
197
+ }
198
+
199
+ const binPath = resolve(packageInfo.packageRoot, binEntry);
200
+ if (!existsSync(binPath)) {
201
+ throw new Error(
202
+ `[KIBI-CURSOR] Project-local ${PACKAGE_NAME} declares a missing executable: ${binPath}. Reinstall the workspace dependency and reload Cursor.`,
203
+ );
204
+ }
205
+
206
+ return {
207
+ packageRoot: packageInfo.packageRoot,
208
+ packageJsonPath: packageInfo.packageJsonPath,
209
+ packageJson: packageInfo.packageJson,
210
+ binPath,
211
+ };
212
+ }
213
+
214
+ function hasDeclaredProjectDependency(workspaceRoot) {
215
+ const packageJsonPath = join(workspaceRoot, "package.json");
216
+ if (!existsSync(packageJsonPath)) return false;
217
+ try {
218
+ const packageJson = readJson(packageJsonPath);
219
+ return [
220
+ packageJson.dependencies,
221
+ packageJson.devDependencies,
222
+ packageJson.optionalDependencies,
223
+ packageJson.peerDependencies,
224
+ ].some((dependencies) =>
225
+ Boolean(
226
+ dependencies &&
227
+ typeof dependencies === "object" &&
228
+ dependencies[PACKAGE_NAME],
229
+ ),
230
+ );
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ function isDemonstrablyProjectWorkspace(workspaceRoot) {
237
+ return (
238
+ existsSync(join(workspaceRoot, ".git")) ||
239
+ existsSync(join(workspaceRoot, ".kb")) ||
240
+ hasDeclaredProjectDependency(workspaceRoot)
241
+ );
242
+ }
243
+
244
+ function usableWorkspaceCandidates(values, baseDirectory) {
245
+ return values
246
+ .map((value) => normalizeWorkspacePath(value, baseDirectory))
247
+ .filter((value, index, all) => value && all.indexOf(value) === index)
248
+ .filter((value) => {
249
+ try {
250
+ resolveProjectLocalMcp(value);
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ });
256
+ }
257
+
258
+ function chooseSingleWorkspace(candidates, source) {
259
+ if (candidates.length > 1) {
260
+ throw new Error(
261
+ `[KIBI-CURSOR] ${source} names multiple workspaces with project-local ${PACKAGE_NAME}: ${candidates.join(", ")}. Open a single-root workspace or set KIBI_WORKSPACE to one root.`,
262
+ );
263
+ }
264
+ return candidates[0];
265
+ }
266
+
267
+ /** Resolve the consumer workspace without ever defaulting to the plugin root. */
268
+ export function resolveWorkspaceRoot(explicitWorkspace, options = {}) {
269
+ const cwd = resolve(options.cwd ?? process.cwd());
270
+ const env = options.env ?? process.env;
271
+
272
+ if (typeof explicitWorkspace === "string" && explicitWorkspace.trim()) {
273
+ const explicit = normalizeWorkspacePath(explicitWorkspace, cwd);
274
+ // Cursor can leave ${workspaceFolder} unresolved. It is invalid, so allow
275
+ // the environment fallbacks below rather than resolving it under cwd.
276
+ if (explicit) return explicit;
277
+ }
278
+
279
+ const folderCandidates = chooseSingleWorkspace(
280
+ usableWorkspaceCandidates(
281
+ parseWorkspaceFolderPaths(env.WORKSPACE_FOLDER_PATHS),
282
+ cwd,
283
+ ),
284
+ "WORKSPACE_FOLDER_PATHS",
285
+ );
286
+ if (folderCandidates) return folderCandidates;
287
+
288
+ for (const name of ["KIBI_WORKSPACE", "CURSOR_WORKSPACE"]) {
289
+ const candidate = chooseSingleWorkspace(
290
+ usableWorkspaceCandidates([env[name]], cwd),
291
+ name,
292
+ );
293
+ if (candidate) return candidate;
294
+ }
295
+
296
+ // A plugin installation is commonly the current cwd. Only use it when
297
+ // project markers make it demonstrably a consumer project and its local
298
+ // package can be resolved; never use an ambient/plugin-local package merely
299
+ // because it happens to be on disk.
300
+ if (isDemonstrablyProjectWorkspace(cwd)) {
301
+ try {
302
+ resolveProjectLocalMcp(cwd);
303
+ return cwd;
304
+ } catch {
305
+ // Continue to the actionable error below.
306
+ }
307
+ }
308
+
309
+ throw new Error(
310
+ `[KIBI-CURSOR] Unable to determine a consumer workspace. Cursor must provide \${workspaceFolder}, WORKSPACE_FOLDER_PATHS, KIBI_WORKSPACE, or CURSOR_WORKSPACE; the selected workspace must contain project-local ${PACKAGE_NAME}.`,
311
+ );
312
+ }
313
+
314
+ function signalExitCode(signal) {
315
+ return SIGNAL_EXIT_CODES[signal] ?? 1;
316
+ }
317
+
318
+ /** Spawn the project-local MCP server and mirror its transport and exit state. */
319
+ export function launchKibiMcp(argv = process.argv.slice(2), env = process.env) {
320
+ const [explicitWorkspace, ...childArgs] = argv;
321
+ let workspaceRoot;
322
+ let projectLocal;
323
+ try {
324
+ workspaceRoot = resolveWorkspaceRoot(explicitWorkspace, {
325
+ cwd: process.cwd(),
326
+ env,
327
+ });
328
+ projectLocal = resolveProjectLocalMcp(workspaceRoot);
329
+ } catch (error) {
330
+ const message = error instanceof Error ? error.message : String(error);
331
+ process.stderr.write(`${message}\n`);
332
+ return Promise.resolve(1);
333
+ }
334
+
335
+ const child = spawn(process.execPath, [projectLocal.binPath, ...childArgs], {
336
+ cwd: workspaceRoot,
337
+ env: { ...env, KIBI_WORKSPACE: workspaceRoot },
338
+ stdio: ["inherit", "inherit", "inherit"],
339
+ });
340
+
341
+ return new Promise((resolveExit) => {
342
+ let finished = false;
343
+ const signalHandlers = new Map();
344
+ const finish = (exitCode) => {
345
+ if (finished) return;
346
+ finished = true;
347
+ for (const [signal, handler] of signalHandlers) {
348
+ process.removeListener(signal, handler);
349
+ }
350
+ resolveExit(exitCode);
351
+ };
352
+
353
+ for (const signal of ["SIGHUP", "SIGINT", "SIGTERM", "SIGBREAK"]) {
354
+ if (process.platform === "win32" && signal !== "SIGBREAK") continue;
355
+ const handler = () => {
356
+ if (!child.killed) child.kill(signal);
357
+ };
358
+ signalHandlers.set(signal, handler);
359
+ process.once(signal, handler);
360
+ }
361
+
362
+ child.once("error", (error) => {
363
+ process.stderr.write(
364
+ `[KIBI-CURSOR] Failed to start project-local ${PACKAGE_NAME}: ${error.message}\n`,
365
+ );
366
+ finish(1);
367
+ });
368
+ child.once("close", (code, signal) => {
369
+ finish(signal ? signalExitCode(signal) : (code ?? 1));
370
+ });
371
+ });
372
+ }
373
+
374
+ const entrypoint = process.argv[1] ? resolve(process.argv[1]) : undefined;
375
+ if (entrypoint === resolve(fileURLToPath(import.meta.url))) {
376
+ const exitCode = await launchKibiMcp(process.argv.slice(2));
377
+ process.exitCode = exitCode;
378
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: kibi-bootstrap
3
+ description: Bootstrap Kibi knowledge for an existing repository
4
+ ---
5
+
6
+ # Kibi bootstrap
7
+
8
+ Route an explicit bootstrap request to the canonical `kibi-bootstrap` skill.
9
+
10
+ ## Route
11
+
12
+ Inspect `kb_status.bootstrap` and follow its typed `nextAction`. If Kibi
13
+ infrastructure is missing, run `kibi init` first. Then use the canonical
14
+ planner and pass its unchanged returned `structuredContent.plan` to
15
+ `kb_apply_plan` only after explicit approval.
16
+
17
+ Questions come only from a `needs_context` planner result. Inspect typed apply
18
+ `nextActions`, finish with `kb_check` and `kb_status`, and never read or edit
19
+ `.kb` directly.
@@ -1,4 +1,5 @@
1
1
  export type HookEvent = "sessionStart" | "preToolUse" | "postToolUse" | "beforeReadFile" | "stop";
2
+ export type HookStopStatus = "completed" | "aborted" | "error";
2
3
  export type HookInput = {
3
4
  event: string;
4
5
  cwd?: string;
@@ -7,6 +8,7 @@ export type HookInput = {
7
8
  toolInput?: unknown;
8
9
  conversationId?: string;
9
10
  filePath?: string;
11
+ status?: HookStopStatus;
10
12
  };
11
13
  export declare function parseHookInput(input: unknown): HookInput;
12
14
  export declare function readStdin(): Promise<string>;
@@ -30,6 +30,12 @@ function normalizeEventName(event) {
30
30
  }
31
31
  return trimmed.charAt(0).toLowerCase() + trimmed.slice(1);
32
32
  }
33
+ function parseStopStatus(value) {
34
+ if (value === "completed" || value === "aborted" || value === "error") {
35
+ return value;
36
+ }
37
+ return undefined;
38
+ }
33
39
  export function parseHookInput(input) {
34
40
  if (!isRecord(input)) {
35
41
  return { event: "" };
@@ -58,6 +64,7 @@ export function parseHookInput(input) {
58
64
  "conversationId",
59
65
  ]);
60
66
  const filePath = readString(input, ["file_path", "filePath", "path"]);
67
+ const status = parseStopStatus(readString(input, ["status"]));
61
68
  const parsed = { event };
62
69
  if (cwd !== undefined) {
63
70
  parsed.cwd = cwd;
@@ -77,6 +84,9 @@ export function parseHookInput(input) {
77
84
  if (filePath !== undefined) {
78
85
  parsed.filePath = filePath;
79
86
  }
87
+ if (status !== undefined) {
88
+ parsed.status = status;
89
+ }
80
90
  return parsed;
81
91
  }
82
92
  export async function readStdin() {
@@ -1,22 +1,24 @@
1
1
  #!/usr/bin/env node
2
- // implements REQ-cursor-kibi-plugin-v1
2
+ // implements REQ-cursor-kibi-plugin-v1, REQ-cursor-stop-job-vs-plan
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  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
- import { addDirtyPaths, clearSessionHookState, hasGuidedPath, loadHookState, recordKbMcpTool, rememberGuidedPath, resolveStateDir, } from "./hook-state.js";
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
- import { extractExplicitPathFields, isDirectKbPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
11
+ import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
12
12
  const editableTools = new Set([
13
13
  "Edit",
14
14
  "MultiEdit",
15
15
  "Write",
16
16
  "StrReplace",
17
17
  "apply_patch",
18
+ "EditNotebook",
18
19
  ]);
19
20
  const readTools = new Set(["Read", "TabRead"]);
21
+ const planDeliveryTools = new Set(["CreatePlan", "create_plan", "createPlan"]);
20
22
  function emptyResult() {
21
23
  return {};
22
24
  }
@@ -24,11 +26,17 @@ function hasKibiConfig(cwd) {
24
26
  if (!cwd) {
25
27
  return false;
26
28
  }
27
- return fs.existsSync(path.join(cwd, ".kb", "config.json"));
29
+ return fs.existsSync(path.join(cwd, ".kb", "manifest.json"));
28
30
  }
29
31
  function isEditLikeTool(toolName) {
30
32
  return toolName === undefined || editableTools.has(toolName);
31
33
  }
34
+ function isKnownEditableTool(toolName) {
35
+ return toolName !== undefined && editableTools.has(toolName);
36
+ }
37
+ function isPlanDeliveryTool(toolName) {
38
+ return toolName !== undefined && planDeliveryTools.has(toolName);
39
+ }
32
40
  function isReadLikeTool(toolName) {
33
41
  return toolName === undefined || readTools.has(toolName);
34
42
  }
@@ -108,12 +116,18 @@ export async function runHook(rawInput, environment = {}) {
108
116
  sourceFiles: kbToolCall.sourceFiles,
109
117
  });
110
118
  }
119
+ if (isPlanDeliveryTool(input.toolName)) {
120
+ recordPlanDelivered(stateDir);
121
+ }
111
122
  const explicitPaths = extractExplicitPathFields(input.toolInput);
112
- const dirtyPaths = explicitPaths
113
- .map((candidate) => toRepoRelativePath(candidate, cwd))
114
- .filter(isMeaningfulTrackedPath);
115
- if (dirtyPaths.length > 0) {
116
- addDirtyPaths(stateDir, dirtyPaths);
123
+ if (isKnownEditableTool(input.toolName)) {
124
+ const dirtyPaths = explicitPaths
125
+ .map((candidate) => toRepoRelativePath(candidate, cwd))
126
+ .filter((candidate) => isMeaningfulTrackedPath(candidate) ||
127
+ isKbFreshnessRelevantPath(candidate));
128
+ if (dirtyPaths.length > 0) {
129
+ addDirtyPaths(stateDir, dirtyPaths);
130
+ }
117
131
  }
118
132
  const primaryPath = resolvePrimaryPath(input);
119
133
  if (!primaryPath || !kibiReady) {
@@ -157,14 +171,22 @@ export async function runHook(rawInput, environment = {}) {
157
171
  }
158
172
  case "stop": {
159
173
  const state = loadHookState(stateDir);
174
+ const shouldClearSession = state.dirtyPaths.length > 0 ||
175
+ state.kbMutationTools.length > 0 ||
176
+ state.kbCheckRun ||
177
+ state.planDelivered;
178
+ if (input.status === "aborted" || input.status === "error") {
179
+ if (shouldClearSession) {
180
+ clearSessionHookState(stateDir);
181
+ }
182
+ return emptyResult();
183
+ }
160
184
  const followupMessage = stopFollowupMessage(state);
161
185
  if (followupMessage) {
162
186
  clearSessionHookState(stateDir);
163
187
  return { followup_message: followupMessage };
164
188
  }
165
- if (state.dirtyPaths.length > 0 ||
166
- state.kbMutationTools.length > 0 ||
167
- state.kbCheckRun) {
189
+ if (shouldClearSession) {
168
190
  clearSessionHookState(stateDir);
169
191
  }
170
192
  return emptyResult();
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ // implements REQ-cursor-kibi-plugin-v1, REQ-cursor-stop-job-vs-plan
5
6
  const stateFileName = "hook-state.json";
6
7
  const lockFileName = "hook-state.lock";
7
8
  export const maxDirtyPaths = 50;
@@ -46,6 +47,7 @@ export function emptyHookState() {
46
47
  kbCheckRun: false,
47
48
  impactCheckRun: false,
48
49
  impactCheckedPaths: [],
50
+ planDelivered: false,
49
51
  };
50
52
  }
51
53
  function acquireLock(pluginData) {
@@ -130,6 +132,7 @@ function coerceHookState(value) {
130
132
  kbCheckRun: value.kbCheckRun === true,
131
133
  impactCheckRun: value.impactCheckRun === true,
132
134
  impactCheckedPaths: [...new Set(impactCheckedPaths)].slice(-maxGuidedPaths),
135
+ planDelivered: value.planDelivered === true,
133
136
  };
134
137
  }
135
138
  export function resolveStateDir(pluginData, conversationId) {
@@ -9,10 +9,12 @@ export type HookState = {
9
9
  kbCheckRun: boolean;
10
10
  impactCheckRun: boolean;
11
11
  impactCheckedPaths: string[];
12
+ planDelivered: boolean;
12
13
  };
13
14
  export declare function addDirtyPaths(stateDir: string | undefined, dirtyPaths: readonly string[]): HookState;
14
15
  export declare function rememberGuidedPath(stateDir: string | undefined, kind: "read" | "write", guidedPath: string): HookState;
15
16
  export declare function hasGuidedPath(state: HookState, kind: "read" | "write", guidedPath: string): boolean;
17
+ export declare function recordPlanDelivered(stateDir: string | undefined): HookState;
16
18
  export declare function recordKbMcpTool(stateDir: string | undefined, toolName: string, options?: {
17
19
  impactCheckRun?: boolean;
18
20
  sourceFiles?: readonly string[];
@@ -29,6 +29,9 @@ export function hasGuidedPath(state, kind, guidedPath) {
29
29
  const bucket = kind === "read" ? state.guidedReadPaths : state.guidedWritePaths;
30
30
  return bucket.includes(normalized);
31
31
  }
32
+ export function recordPlanDelivered(stateDir) {
33
+ return updateHookState(stateDir, (state) => state.planDelivered ? state : { ...state, planDelivered: true });
34
+ }
32
35
  export function recordKbMcpTool(stateDir, toolName, options = {}) {
33
36
  const normalized = toolName.trim();
34
37
  if (normalized.length === 0) {
package/dist/mcp.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "kibi": {
4
- "command": "npx",
5
- "args": ["--no-install", "kibi-mcp"]
4
+ "command": "node",
5
+ "args": ["bin/launch-kibi-mcp.mjs", "${workspaceFolder}"]
6
6
  }
7
7
  }
8
8
  }
package/dist/messages.js CHANGED
@@ -20,13 +20,18 @@ export function stopFollowupMessage(state) {
20
20
  }
21
21
  const sourceImpactPaths = state.dirtyPaths.filter(isSourceImpactRelevantPath);
22
22
  const uncheckedSourcePaths = sourceImpactPaths.filter((sourcePath) => !state.impactCheckedPaths.includes(sourcePath));
23
+ const freshnessPaths = state.dirtyPaths.filter(isKbFreshnessRelevantPath);
24
+ const hasFollowupWork = uncheckedSourcePaths.length > 0 ||
25
+ (freshnessPaths.length > 0 && !state.kbCheckRun);
26
+ if (state.planDelivered && !hasFollowupWork) {
27
+ return undefined;
28
+ }
23
29
  if (uncheckedSourcePaths.length > 0 && !state.impactCheckRun) {
24
30
  return impactCheckFollowup(uncheckedSourcePaths);
25
31
  }
26
32
  if (uncheckedSourcePaths.length > 0) {
27
33
  return impactCheckFollowup(uncheckedSourcePaths);
28
34
  }
29
- const freshnessPaths = state.dirtyPaths.filter(isKbFreshnessRelevantPath);
30
35
  if (freshnessPaths.length === 0 || state.kbCheckRun) {
31
36
  return undefined;
32
37
  }
@@ -41,6 +41,30 @@ const sourceExtensions = new Set([
41
41
  ".vue",
42
42
  ]);
43
43
  const documentationExtensions = new Set([".md", ".mdx", ".rst", ".txt"]);
44
+ const CANONICAL_KB_KNOWLEDGE_LANES = new Set([
45
+ "requirements",
46
+ "scenarios",
47
+ "tests",
48
+ "facts",
49
+ "adr",
50
+ "flags",
51
+ "events",
52
+ ]);
53
+ const CANONICAL_KB_KNOWLEDGE_FILES = new Set([
54
+ "symbols.yaml",
55
+ "symbol-coordinates.yaml",
56
+ ]);
57
+ function isCanonicalKbKnowledgePath(segments) {
58
+ if (segments[0] !== ".kb") {
59
+ return false;
60
+ }
61
+ const lane = segments[1];
62
+ if (lane === undefined) {
63
+ return false;
64
+ }
65
+ return (CANONICAL_KB_KNOWLEDGE_FILES.has(lane) ||
66
+ CANONICAL_KB_KNOWLEDGE_LANES.has(lane));
67
+ }
44
68
  function isRecord(value) {
45
69
  return typeof value === "object" && value !== null && !Array.isArray(value);
46
70
  }
@@ -93,9 +117,12 @@ export function isDirectKbPath(candidate) {
93
117
  export function isMeaningfulTrackedPath(candidate) {
94
118
  const normalized = normalizePath(candidate);
95
119
  const segments = pathSegments(normalized);
96
- if (segments.includes(".kb") || segments.includes("dist")) {
120
+ if (segments.includes("dist")) {
97
121
  return false;
98
122
  }
123
+ if (segments.includes(".kb")) {
124
+ return isCanonicalKbKnowledgePath(segments);
125
+ }
99
126
  const basename = segments.at(-1) ?? "";
100
127
  const extension = basename.includes(".")
101
128
  ? `.${basename.split(".").at(-1) ?? ""}`
@@ -116,6 +143,21 @@ export function isMeaningfulTrackedPath(candidate) {
116
143
  /** Paths whose edits should trigger a KB freshness stop follow-up. */
117
144
  export function isKbFreshnessRelevantPath(candidate) {
118
145
  const segments = pathSegments(normalizePath(candidate));
146
+ if (segments[0] === ".kb") {
147
+ const lane = segments[1];
148
+ if (lane === "requirements" ||
149
+ lane === "scenarios" ||
150
+ lane === "tests" ||
151
+ lane === "facts" ||
152
+ lane === "adr" ||
153
+ lane === "flags" ||
154
+ lane === "events" ||
155
+ lane === "symbols.yaml" ||
156
+ lane === "symbol-coordinates.yaml") {
157
+ return true;
158
+ }
159
+ }
160
+ // Legacy layout during migration
119
161
  if (segments.includes("documentation")) {
120
162
  return true;
121
163
  }