kibi-mcp 3.2.0 → 3.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.
|
@@ -64,6 +64,7 @@ export function createMcpRuntime(session) {
|
|
|
64
64
|
...(fs ? { sourceFirst: true } : {}),
|
|
65
65
|
...(git ? { git } : {}),
|
|
66
66
|
...(net ? { net } : {}),
|
|
67
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
67
68
|
};
|
|
68
69
|
const resolved = resolveMcpAttachment(context.workspaceRoot, spec);
|
|
69
70
|
const attachment = resolved instanceof Promise ? await resolved : resolved;
|
|
@@ -13,23 +13,92 @@ export function singleLiteralOrAny(literalSchemas, description) {
|
|
|
13
13
|
}
|
|
14
14
|
return description ? single.describe(description) : single;
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
function matchesJsonType(value, type) {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "object":
|
|
19
|
+
return (value !== null && typeof value === "object" && !Array.isArray(value));
|
|
20
|
+
case "array":
|
|
21
|
+
return Array.isArray(value);
|
|
22
|
+
case "string":
|
|
23
|
+
return typeof value === "string";
|
|
24
|
+
case "number":
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
26
|
+
case "integer":
|
|
27
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
28
|
+
case "boolean":
|
|
29
|
+
return typeof value === "boolean";
|
|
30
|
+
case "null":
|
|
31
|
+
return value === null;
|
|
32
|
+
default:
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function sameJsonValue(left, right) {
|
|
37
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
38
|
+
}
|
|
39
|
+
// Evaluates a JSON Schema `if` condition the way a JSON Schema validator
|
|
40
|
+
// would for the keywords Kibi's operation schemas use in conditionals:
|
|
41
|
+
// boolean schemas, type, const, enum, required, properties (applied only to
|
|
42
|
+
// present keys), not, anyOf, allOf and oneOf. Keywords outside this set do not
|
|
43
|
+
// constrain the condition. The CLI validates the same schemas with ajv, so
|
|
44
|
+
// this must stay faithful: a mismatch makes MCP reject payloads the CLI
|
|
45
|
+
// accepts (or the reverse).
|
|
46
|
+
// implements REQ-002, REQ-mcp-conditional-input-validation
|
|
47
|
+
export function matchesJsonSchemaCondition(value, schema) {
|
|
48
|
+
if (schema === true || schema === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
if (schema === false)
|
|
18
51
|
return false;
|
|
52
|
+
if (schema === null || typeof schema !== "object")
|
|
53
|
+
return true;
|
|
19
54
|
const condition = schema;
|
|
20
|
-
const
|
|
21
|
-
? condition.
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
const types = Array.isArray(condition.type)
|
|
56
|
+
? condition.type.filter((entry) => typeof entry === "string")
|
|
57
|
+
: typeof condition.type === "string"
|
|
58
|
+
? [condition.type]
|
|
59
|
+
: [];
|
|
60
|
+
if (types.length > 0 && !types.some((type) => matchesJsonType(value, type))) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (Object.hasOwn(condition, "const") &&
|
|
64
|
+
!sameJsonValue(value, condition.const)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(condition.enum) &&
|
|
68
|
+
!condition.enum.some((entry) => sameJsonValue(value, entry))) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
72
|
+
if (isObject) {
|
|
73
|
+
const record = value;
|
|
74
|
+
const required = Array.isArray(condition.required)
|
|
75
|
+
? condition.required.filter((key) => typeof key === "string" && key.length > 0)
|
|
76
|
+
: [];
|
|
77
|
+
if (!required.every((key) => Object.hasOwn(record, key)))
|
|
78
|
+
return false;
|
|
79
|
+
if (condition.properties && typeof condition.properties === "object") {
|
|
80
|
+
for (const [key, propertySchema] of Object.entries(condition.properties)) {
|
|
81
|
+
if (Object.hasOwn(record, key) &&
|
|
82
|
+
!matchesJsonSchemaCondition(record[key], propertySchema)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if ("not" in condition && matchesJsonSchemaCondition(value, condition.not)) {
|
|
25
89
|
return false;
|
|
26
90
|
}
|
|
27
91
|
if (Array.isArray(condition.anyOf) &&
|
|
28
|
-
!condition.anyOf.some((entry) =>
|
|
92
|
+
!condition.anyOf.some((entry) => matchesJsonSchemaCondition(value, entry))) {
|
|
29
93
|
return false;
|
|
30
94
|
}
|
|
31
95
|
if (Array.isArray(condition.allOf) &&
|
|
32
|
-
!condition.allOf.every((entry) =>
|
|
96
|
+
!condition.allOf.every((entry) => matchesJsonSchemaCondition(value, entry))) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(condition.oneOf) &&
|
|
100
|
+
condition.oneOf.filter((entry) => matchesJsonSchemaCondition(value, entry))
|
|
101
|
+
.length !== 1) {
|
|
33
102
|
return false;
|
|
34
103
|
}
|
|
35
104
|
return true;
|
|
@@ -40,7 +109,7 @@ function conditionalRequiredKeys(value, condition) {
|
|
|
40
109
|
const rule = condition;
|
|
41
110
|
if (!("if" in rule) || !("then" in rule))
|
|
42
111
|
return [];
|
|
43
|
-
if (!
|
|
112
|
+
if (!matchesJsonSchemaCondition(value, rule.if))
|
|
44
113
|
return [];
|
|
45
114
|
if (rule.then === null || typeof rule.then !== "object")
|
|
46
115
|
return [];
|
|
@@ -49,7 +118,68 @@ function conditionalRequiredKeys(value, condition) {
|
|
|
49
118
|
? required.filter((key) => typeof key === "string" && key.length > 0)
|
|
50
119
|
: [];
|
|
51
120
|
}
|
|
52
|
-
|
|
121
|
+
const GUARD_KEYWORDS = new Set([
|
|
122
|
+
"required",
|
|
123
|
+
"not",
|
|
124
|
+
"anyOf",
|
|
125
|
+
"allOf",
|
|
126
|
+
"description",
|
|
127
|
+
]);
|
|
128
|
+
// A guard branch only states which keys must or must not be present
|
|
129
|
+
// (required/not/anyOf/allOf of such conditions). It carries no property
|
|
130
|
+
// schemas, so enforcing a oneOf of guard branches can only tighten
|
|
131
|
+
// validation, never loosen it.
|
|
132
|
+
function isGuardBranch(branch) {
|
|
133
|
+
if (branch === null || typeof branch !== "object" || Array.isArray(branch))
|
|
134
|
+
return false;
|
|
135
|
+
const record = branch;
|
|
136
|
+
const keys = Object.keys(record);
|
|
137
|
+
if (keys.length === 0)
|
|
138
|
+
return false;
|
|
139
|
+
return keys.every((key) => {
|
|
140
|
+
if (!GUARD_KEYWORDS.has(key))
|
|
141
|
+
return false;
|
|
142
|
+
const entry = record[key];
|
|
143
|
+
if (key === "required")
|
|
144
|
+
return (Array.isArray(entry) && entry.every((item) => typeof item === "string"));
|
|
145
|
+
if (key === "not")
|
|
146
|
+
return isGuardBranch(entry);
|
|
147
|
+
if (key === "anyOf" || key === "allOf")
|
|
148
|
+
return Array.isArray(entry) && entry.every(isGuardBranch);
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function guardOneOfBranches(obj) {
|
|
153
|
+
if (!Array.isArray(obj.oneOf) || obj.oneOf.length < 2)
|
|
154
|
+
return [];
|
|
155
|
+
return obj.oneOf.every(isGuardBranch) ? obj.oneOf : [];
|
|
156
|
+
}
|
|
157
|
+
function describeGuardBranch(branch) {
|
|
158
|
+
const required = Array.isArray(branch.required)
|
|
159
|
+
? branch.required
|
|
160
|
+
: [];
|
|
161
|
+
const forbidden = new Set();
|
|
162
|
+
const collect = (entry) => {
|
|
163
|
+
if (entry === null || typeof entry !== "object")
|
|
164
|
+
return;
|
|
165
|
+
const record = entry;
|
|
166
|
+
if (Array.isArray(record.required))
|
|
167
|
+
for (const key of record.required)
|
|
168
|
+
forbidden.add(key);
|
|
169
|
+
if (Array.isArray(record.anyOf))
|
|
170
|
+
for (const item of record.anyOf)
|
|
171
|
+
collect(item);
|
|
172
|
+
if (Array.isArray(record.allOf))
|
|
173
|
+
for (const item of record.allOf)
|
|
174
|
+
collect(item);
|
|
175
|
+
};
|
|
176
|
+
collect(branch.not);
|
|
177
|
+
const parts = [`{${required.join(", ")}}`];
|
|
178
|
+
if (forbidden.size > 0)
|
|
179
|
+
parts.push(`without ${[...forbidden].join(", ")}`);
|
|
180
|
+
return parts.join(" ");
|
|
181
|
+
}
|
|
182
|
+
// implements REQ-002, REQ-mcp-oneof-guard-input-validation
|
|
53
183
|
export function jsonSchemaToZod(schema) {
|
|
54
184
|
if (!schema || typeof schema !== "object") {
|
|
55
185
|
return z.any();
|
|
@@ -91,9 +221,10 @@ export function jsonSchemaToZod(schema) {
|
|
|
91
221
|
// JSON Schema anyOf unions become real Zod unions so MCP output
|
|
92
222
|
// validation enforces the declared alternatives instead of silently
|
|
93
223
|
// degrading to z.any() — e.g. kb_check's synchronous payload versus its
|
|
94
|
-
// kibi.job.v1 async receipt. oneOf is
|
|
95
|
-
// required-guard branches carry no properties and would loosen
|
|
96
|
-
// validation
|
|
224
|
+
// kibi.job.v1 async receipt. oneOf is not turned into a union: its
|
|
225
|
+
// required-guard branches carry no properties and a union would loosen
|
|
226
|
+
// input validation. Guard oneOf branches on an object are enforced as a
|
|
227
|
+
// refinement in the object case below instead.
|
|
97
228
|
const anyOfVariants = Array.isArray(obj.anyOf) ? obj.anyOf : [];
|
|
98
229
|
if (anyOfVariants.length > 0) {
|
|
99
230
|
const variants = anyOfVariants.map((variant) => jsonSchemaToZod(variant));
|
|
@@ -149,6 +280,27 @@ export function jsonSchemaToZod(schema) {
|
|
|
149
280
|
})
|
|
150
281
|
.meta({ allOf: obj.allOf });
|
|
151
282
|
}
|
|
283
|
+
// A top-level oneOf of guard branches (for example "plan with
|
|
284
|
+
// approvedPlanHash, or recoveryJournalId alone") is enforced with the
|
|
285
|
+
// same condition matcher as if/then: exactly one branch must match.
|
|
286
|
+
// It is not republished as JSON Schema metadata, because hosts reject
|
|
287
|
+
// a top-level oneOf in a tool input schema; the CLI enforces the same
|
|
288
|
+
// rule with ajv.
|
|
289
|
+
const guardBranches = guardOneOfBranches(obj);
|
|
290
|
+
if (guardBranches.length > 0) {
|
|
291
|
+
result = result.superRefine((value, context) => {
|
|
292
|
+
if (value === null || typeof value !== "object")
|
|
293
|
+
return;
|
|
294
|
+
const matches = guardBranches.filter((branch) => matchesJsonSchemaCondition(value, branch)).length;
|
|
295
|
+
if (matches === 1)
|
|
296
|
+
return;
|
|
297
|
+
context.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
path: [],
|
|
300
|
+
message: `Input must match exactly one of: ${guardBranches.map(describeGuardBranch).join("; ")} (matched ${matches}).`,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
152
304
|
return result;
|
|
153
305
|
}
|
|
154
306
|
case "array": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getSpec, statusSpec } from "kibi-runtime";
|
|
2
|
-
import { MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, } from "kibi-runtime";
|
|
2
|
+
import { MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, preflightApplyPlan, } from "kibi-runtime";
|
|
3
3
|
import { OPTIONAL_TOOL_NAMES, enabledOptionalTools } from "../tools-config.js";
|
|
4
4
|
import { startJob } from "./jobs.js";
|
|
5
5
|
// implements REQ-002, REQ-013
|
|
@@ -150,9 +150,37 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
150
150
|
});
|
|
151
151
|
register({
|
|
152
152
|
name: "kb_apply_plan",
|
|
153
|
-
execute: async (context, args) =>
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
execute: async (context, args) => {
|
|
154
|
+
const { async: asyncMode, ...applyArgs } = args;
|
|
155
|
+
const apply = (applyContext) => runtime.handleKbApplyPlan
|
|
156
|
+
? runtime.handleKbApplyPlan(applyArgs, applyContext)
|
|
157
|
+
: executeApplyPlan(applyArgs, applyContext);
|
|
158
|
+
// Without a registered kb_job_status nothing could poll the receipt,
|
|
159
|
+
// so async falls back to a synchronous apply (which still reports
|
|
160
|
+
// progress when the request carries a progressToken).
|
|
161
|
+
if (asyncMode !== true || !enabledOptionalTools().has("kb_job_status")) {
|
|
162
|
+
return apply(context);
|
|
163
|
+
}
|
|
164
|
+
// A large bootstrap plan can outlast any client request timeout.
|
|
165
|
+
// Detach it into a job and return the kibi.job.v1 receipt; the agent
|
|
166
|
+
// polls kb_job_status. The request is over once the receipt returns,
|
|
167
|
+
// so the job reports no progress and refreshes the branch stamp itself.
|
|
168
|
+
// Input and stale-plan errors (missing or wrong approvedPlanHash, a
|
|
169
|
+
// changed snapshot) fail this call rather than the job; the apply
|
|
170
|
+
// repeats every check under the workspace lock.
|
|
171
|
+
await (runtime.preflightApplyPlan ?? preflightApplyPlan)(applyArgs, context);
|
|
172
|
+
const { onProgress: _requestProgress, ...jobContext } = context;
|
|
173
|
+
return startJob("kb_apply_plan", async () => {
|
|
174
|
+
const result = await apply(jobContext);
|
|
175
|
+
await runtime.operationRuntime.afterSuccess({
|
|
176
|
+
name: "kb_apply_plan",
|
|
177
|
+
effects: getSpec("kb_apply_plan").effects,
|
|
178
|
+
requiresProlog: true,
|
|
179
|
+
execute: async () => undefined,
|
|
180
|
+
}, jobContext);
|
|
181
|
+
return result;
|
|
182
|
+
});
|
|
183
|
+
},
|
|
156
184
|
});
|
|
157
185
|
register({
|
|
158
186
|
name: "kb_ingest_proof",
|
package/dist/server/tools.js
CHANGED
|
@@ -85,12 +85,14 @@ function isToolTimeoutError(value) {
|
|
|
85
85
|
value.code === "KIBI_TOOL_TIMEOUT");
|
|
86
86
|
}
|
|
87
87
|
// implements REQ-002
|
|
88
|
-
async function withToolTimeout(toolName, operation, onTimeout) {
|
|
88
|
+
async function withToolTimeout(toolName, operation, onTimeout, activity) {
|
|
89
89
|
const timeoutMs = getToolTimeoutMs();
|
|
90
90
|
let timeout;
|
|
91
|
+
let fired = false;
|
|
91
92
|
try {
|
|
92
93
|
const timeoutPromise = new Promise((_, reject) => {
|
|
93
|
-
|
|
94
|
+
const fire = () => {
|
|
95
|
+
fired = true;
|
|
94
96
|
const error = createToolTimeoutError(toolName, timeoutMs);
|
|
95
97
|
// Do not report the timeout until cancellation/reset has completed
|
|
96
98
|
// and the original request has reached a terminal state. This is
|
|
@@ -105,7 +107,17 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
105
107
|
]);
|
|
106
108
|
})
|
|
107
109
|
.finally(() => reject(error));
|
|
108
|
-
}
|
|
110
|
+
};
|
|
111
|
+
timeout = setTimeout(fire, timeoutMs);
|
|
112
|
+
if (activity) {
|
|
113
|
+
activity.touch = () => {
|
|
114
|
+
if (fired)
|
|
115
|
+
return;
|
|
116
|
+
if (timeout)
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
timeout = setTimeout(fire, timeoutMs);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
109
121
|
});
|
|
110
122
|
// When abort settles `operation` before this timeout promise rejects,
|
|
111
123
|
// the late reject must not become an unhandled rejection.
|
|
@@ -118,6 +130,35 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Forward operation progress as MCP notifications/progress when the request
|
|
135
|
+
* carries a progressToken, and push the server-side tool timeout back on each
|
|
136
|
+
* report. Progress values must increase, so repeats are nudged forward.
|
|
137
|
+
*/
|
|
138
|
+
// implements REQ-bootstrap-apply-long-running
|
|
139
|
+
export function progressReporter(extra, activity) {
|
|
140
|
+
const token = extra?._meta?.progressToken;
|
|
141
|
+
const send = extra?.sendNotification;
|
|
142
|
+
if (token === undefined || typeof send !== "function")
|
|
143
|
+
return undefined;
|
|
144
|
+
let last = -1;
|
|
145
|
+
return ({ progress, total, message }) => {
|
|
146
|
+
activity.touch();
|
|
147
|
+
const value = progress > last ? progress : last + 0.001;
|
|
148
|
+
last = value;
|
|
149
|
+
void send({
|
|
150
|
+
method: "notifications/progress",
|
|
151
|
+
params: {
|
|
152
|
+
progressToken: token,
|
|
153
|
+
progress: value,
|
|
154
|
+
...(total === undefined ? {} : { total }),
|
|
155
|
+
...(message === undefined ? {} : { message }),
|
|
156
|
+
},
|
|
157
|
+
}).catch((error) => {
|
|
158
|
+
debugLog("[KIBI-MCP] progress notification failed:", error instanceof Error ? error.message : String(error));
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
}
|
|
121
162
|
/**
|
|
122
163
|
* A module that vanished after startup means the package was upgraded or
|
|
123
164
|
* reinstalled under a running server; only a restart loads the new install.
|
|
@@ -177,7 +218,7 @@ export function addTool(server, name, description, inputSchema, handler,
|
|
|
177
218
|
// generic TProlog parameter exists so tests can inject a mock type. The cast is safe
|
|
178
219
|
// because the runtime object satisfies the full ToolsRuntime contract at runtime.
|
|
179
220
|
runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
180
|
-
const wrappedHandler = async (rawArgs) => {
|
|
221
|
+
const wrappedHandler = async (rawArgs, extra) => {
|
|
181
222
|
const startedAt = new Date();
|
|
182
223
|
const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
|
|
183
224
|
let args = rawArgs;
|
|
@@ -228,7 +269,12 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
228
269
|
execute: async (input, _context) => handler(input),
|
|
229
270
|
};
|
|
230
271
|
const observed = observeDetachedReads(runtime.operationRuntime);
|
|
231
|
-
const
|
|
272
|
+
const activity = { touch: () => undefined };
|
|
273
|
+
const onProgress = progressReporter(extra, activity);
|
|
274
|
+
const handlerPromise = executeOperation(observed.runtime, operationSpec, businessArgs, {
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
...(onProgress ? { onProgress } : {}),
|
|
277
|
+
});
|
|
232
278
|
trackedRequests.set(requestId, handlerPromise);
|
|
233
279
|
let resetAttempted = false;
|
|
234
280
|
let resetSucceeded = false;
|
|
@@ -261,7 +307,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
261
307
|
? resetFailure.message
|
|
262
308
|
: String(resetFailure);
|
|
263
309
|
}
|
|
264
|
-
});
|
|
310
|
+
}, activity);
|
|
265
311
|
const data = operationData(result);
|
|
266
312
|
const envelope = withWorkspaceNotice(withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice), observed.notice());
|
|
267
313
|
// Log usage in diagnostic mode
|
|
@@ -414,12 +460,12 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
414
460
|
}
|
|
415
461
|
/**
|
|
416
462
|
* kb_job_status polls background jobs started by long-running operations
|
|
417
|
-
* (
|
|
463
|
+
* (`kb_check` and `kb_apply_plan` with `async: true`). This is an MCP-server-native
|
|
418
464
|
* companion to the job receipts, not part of the canonical CLI operation
|
|
419
465
|
* catalog: jobs live in the server process and have no CLI counterpart.
|
|
420
466
|
*/
|
|
421
467
|
function registerJobStatusTool(server) {
|
|
422
|
-
addTool(server, "kb_job_status", "Poll a background job started with async:true (
|
|
468
|
+
addTool(server, "kb_job_status", "Poll a background job started with async:true (kb_check or kb_apply_plan async jobs). Returns the kibi.job.v1 state: running, succeeded (with the full result), or failed (with the error).", {
|
|
423
469
|
type: "object",
|
|
424
470
|
properties: {
|
|
425
471
|
jobId: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"mcpName": "io.github.Looted/kibi-mcp",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"fast-glob": "^3.2.12",
|
|
11
11
|
"gray-matter": "^4.0.3",
|
|
12
12
|
"js-yaml": "^4.1.0",
|
|
13
|
-
"kibi-runtime": "^2.
|
|
14
|
-
"kibi-core": "^0.
|
|
13
|
+
"kibi-runtime": "^2.8.0",
|
|
14
|
+
"kibi-core": "^0.17.0",
|
|
15
15
|
"zod": "^4.3.6"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|