@alisio/core 0.7.0 → 0.8.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/dist/application.d.ts +15 -0
- package/dist/application.js +38 -3
- package/dist/changelog/data.js +23 -0
- package/dist/commands/builtins.js +7 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +32 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +57 -1
- package/dist/eval/engine.d.ts +1 -0
- package/dist/eval/engine.js +3 -0
- package/dist/eval/secrets.d.ts +2 -0
- package/dist/eval/secrets.js +10 -0
- package/dist/goal/command.d.ts +7 -2
- package/dist/goal/command.js +10 -2
- package/dist/goal/format.js +2 -0
- package/dist/goal/machine.d.ts +50 -3
- package/dist/goal/machine.js +80 -2
- package/dist/goal/service.d.ts +22 -2
- package/dist/goal/service.js +94 -7
- package/dist/goal/store.js +22 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plan/criteria.d.ts +30 -0
- package/dist/plan/criteria.js +256 -0
- package/dist/plan/exit-plan.d.ts +8 -0
- package/dist/plan/exit-plan.js +89 -8
- package/dist/plan/manifest.d.ts +4 -2
- package/dist/plan/manifest.js +6 -1
- package/dist/plan/state.d.ts +13 -2
- package/dist/plan/state.js +7 -1
- package/dist/plugins/host.d.ts +7 -0
- package/dist/plugins/host.js +15 -0
- package/dist/proof/command.d.ts +24 -0
- package/dist/proof/command.js +109 -0
- package/dist/proof/coverage.d.ts +40 -0
- package/dist/proof/coverage.js +102 -0
- package/dist/proof/divergence.d.ts +24 -0
- package/dist/proof/divergence.js +120 -0
- package/dist/proof/export.d.ts +65 -0
- package/dist/proof/export.js +144 -0
- package/dist/proof/index.d.ts +12 -0
- package/dist/proof/index.js +12 -0
- package/dist/proof/ledger.d.ts +38 -0
- package/dist/proof/ledger.js +57 -0
- package/dist/proof/plugin-verifiers.d.ts +23 -0
- package/dist/proof/plugin-verifiers.js +39 -0
- package/dist/proof/recorder.d.ts +69 -0
- package/dist/proof/recorder.js +347 -0
- package/dist/proof/scope.d.ts +12 -0
- package/dist/proof/scope.js +121 -0
- package/dist/proof/store.d.ts +52 -0
- package/dist/proof/store.js +192 -0
- package/dist/proof/tree.d.ts +6 -0
- package/dist/proof/tree.js +78 -0
- package/dist/proof/types.d.ts +21 -0
- package/dist/proof/types.js +1 -0
- package/dist/proof/verifiers.d.ts +46 -0
- package/dist/proof/verifiers.js +176 -0
- package/dist/proof/verify.d.ts +36 -0
- package/dist/proof/verify.js +171 -0
- package/dist/runtime/store.js +60 -0
- package/dist/tools/goal.d.ts +5 -1
- package/dist/tools/goal.js +35 -4
- package/package.json +2 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a verifier on demand (specs/011 3.1, 3.8). This is a USER-side entry point: no tool
|
|
3
|
+
* exposes it to the model, so the model can neither start a verification nor choose its verdict.
|
|
4
|
+
*
|
|
5
|
+
* - `file`, `data-check` and (opt-in) `coverage` run their canonical core tool call (`verifierCall`) as a real run
|
|
6
|
+
* under the normal gate; the recorder captures the evidence from the raw result like it does
|
|
7
|
+
* for a call the model made (D3, D6).
|
|
8
|
+
* - `plugin` runs the registered verifier. Its calls go through `callTool` (the same gate), the
|
|
9
|
+
* result must carry referenceable evidence, and anything else degrades the criterion to
|
|
10
|
+
* `manual_review` (an evidence of kind `manual`). A verifier that throws or hangs proves
|
|
11
|
+
* nothing: the criterion stays as it was and the outcome says why.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { redactOutput } from "../eval/secrets.js";
|
|
15
|
+
import { excerptOf } from "./recorder.js";
|
|
16
|
+
import { treeFingerprint } from "./tree.js";
|
|
17
|
+
import { verifierCall } from "./verifiers.js";
|
|
18
|
+
const PLUGIN_TIMEOUT_MS = 60_000;
|
|
19
|
+
const HEX_SHA256 = /^[0-9a-f]{64}$/i;
|
|
20
|
+
const DETAIL_MAX = 200;
|
|
21
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
22
|
+
function describeError(error) {
|
|
23
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
24
|
+
return redactOutput(text).slice(0, DETAIL_MAX);
|
|
25
|
+
}
|
|
26
|
+
export async function verifyCriterion(deps, request) {
|
|
27
|
+
const { criterionId, sessionId } = request;
|
|
28
|
+
const statusOf = () => {
|
|
29
|
+
const ledger = deps.store.ledger(sessionId, (deps.now?.() ?? new Date()).toISOString());
|
|
30
|
+
const criterion = ledger.criteria.find((c) => c.criterionId === criterionId);
|
|
31
|
+
return { status: criterion?.status ?? "not_checked" };
|
|
32
|
+
};
|
|
33
|
+
const outcome = (reason, detail, evidenceId) => ({
|
|
34
|
+
criterionId,
|
|
35
|
+
...statusOf(),
|
|
36
|
+
...(evidenceId ? { evidenceId } : {}),
|
|
37
|
+
...(reason ? { reason } : {}),
|
|
38
|
+
...(detail ? { detail } : {}),
|
|
39
|
+
});
|
|
40
|
+
const record = deps.store.criterion(criterionId);
|
|
41
|
+
if (!record || record.sessionId !== sessionId)
|
|
42
|
+
return outcome("unknown_criterion");
|
|
43
|
+
if (record.notApplicableReason)
|
|
44
|
+
return outcome("not_applicable");
|
|
45
|
+
const verifier = record.verifier;
|
|
46
|
+
if (!verifier)
|
|
47
|
+
return outcome("no_verifier");
|
|
48
|
+
const signal = request.signal ?? new AbortController().signal;
|
|
49
|
+
if (verifier.kind === "coverage" && deps.coverage?.() !== true)
|
|
50
|
+
return outcome("coverage_disabled", "proof.coverage.enabled is false");
|
|
51
|
+
if (verifier.kind === "file" || verifier.kind === "data-check" || verifier.kind === "coverage") {
|
|
52
|
+
const call = verifierCall(verifier);
|
|
53
|
+
if (!call)
|
|
54
|
+
return outcome("unverifiable_spec");
|
|
55
|
+
let done;
|
|
56
|
+
try {
|
|
57
|
+
done = await deps.callTool(sessionId, call.name, call.input, signal);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return outcome("verifier_failed", describeError(error));
|
|
61
|
+
}
|
|
62
|
+
const evidence = deps.store
|
|
63
|
+
.ledger(sessionId)
|
|
64
|
+
.evidence.find((e) => e.criterionId === criterionId && e.callId === done.callId);
|
|
65
|
+
return evidence ? outcome(undefined, undefined, evidence.evidenceId) : outcome("no_evidence");
|
|
66
|
+
}
|
|
67
|
+
if (verifier.kind === "plugin") {
|
|
68
|
+
const entry = deps.registry.get(verifier.verifierId);
|
|
69
|
+
if (!entry)
|
|
70
|
+
return outcome("verifier_unavailable");
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
const timeout = AbortSignal.timeout(deps.timeoutMs ?? PLUGIN_TIMEOUT_MS);
|
|
73
|
+
const combined = AbortSignal.any([signal, timeout]);
|
|
74
|
+
const context = {
|
|
75
|
+
sessionId,
|
|
76
|
+
criterionId,
|
|
77
|
+
workspace: request.workspace,
|
|
78
|
+
signal: combined,
|
|
79
|
+
callTool: async (name, input) => {
|
|
80
|
+
const { callId, result } = await deps.callTool(sessionId, name, input, combined);
|
|
81
|
+
return { callId, result };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
let result;
|
|
85
|
+
try {
|
|
86
|
+
result = await raceAbort(entry.verifier.run(verifier.input, context), combined);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return outcome("verifier_failed", describeError(error));
|
|
90
|
+
}
|
|
91
|
+
const durationMs = Date.now() - started;
|
|
92
|
+
const checked = referenceable(result, deps.db, sessionId);
|
|
93
|
+
const filesTouched = deps.store.edits(sessionId).map((e) => e.path);
|
|
94
|
+
const base = {
|
|
95
|
+
criterionId,
|
|
96
|
+
durationMs,
|
|
97
|
+
treeFingerprint: treeFingerprint(request.workspace, filesTouched),
|
|
98
|
+
filesTouched,
|
|
99
|
+
capturedAt: (deps.now?.() ?? new Date()).toISOString(),
|
|
100
|
+
};
|
|
101
|
+
if (checked) {
|
|
102
|
+
const excerpt = typeof checked.excerpt === "string" ? excerptOf(checked.excerpt) : undefined;
|
|
103
|
+
const evidence = deps.store.appendEvidence({
|
|
104
|
+
...base,
|
|
105
|
+
verifierKind: "plugin",
|
|
106
|
+
runId: checked.runId,
|
|
107
|
+
callId: checked.callId,
|
|
108
|
+
ok: checked.ok,
|
|
109
|
+
outputHash: checked.outputHash,
|
|
110
|
+
...(excerpt ? { excerpt } : {}),
|
|
111
|
+
});
|
|
112
|
+
return outcome(undefined, undefined, evidence.evidenceId);
|
|
113
|
+
}
|
|
114
|
+
const why = `verifier ${verifier.verifierId} returned no traceable evidence`;
|
|
115
|
+
const evidence = deps.store.appendEvidence({
|
|
116
|
+
...base,
|
|
117
|
+
verifierKind: "manual",
|
|
118
|
+
runId: `verify_${randomUUID()}`,
|
|
119
|
+
callId: `verify_${randomUUID()}`,
|
|
120
|
+
ok: false,
|
|
121
|
+
outputHash: sha256(why),
|
|
122
|
+
excerpt: why,
|
|
123
|
+
});
|
|
124
|
+
return outcome("no_referenceable_evidence", undefined, evidence.evidenceId);
|
|
125
|
+
}
|
|
126
|
+
return outcome("unsupported_verifier");
|
|
127
|
+
}
|
|
128
|
+
/** Rejects as soon as `signal` aborts, even when the verifier ignores it. */
|
|
129
|
+
function raceAbort(work, signal) {
|
|
130
|
+
if (signal.aborted)
|
|
131
|
+
return Promise.reject(signal.reason ?? new Error("aborted"));
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
134
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
work.then((value) => {
|
|
136
|
+
signal.removeEventListener("abort", onAbort);
|
|
137
|
+
resolve(value);
|
|
138
|
+
}, (error) => {
|
|
139
|
+
signal.removeEventListener("abort", onAbort);
|
|
140
|
+
reject(error);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A plugin result is evidence only with a boolean verdict and a sha256 `outputHash`; a `callId`
|
|
146
|
+
* must name a call this session journaled (its run id is then the real one). Without a callId the
|
|
147
|
+
* host mints a verification id: the evidence is then the plugin's word plus the hash.
|
|
148
|
+
*/
|
|
149
|
+
function referenceable(result, db, sessionId) {
|
|
150
|
+
if (typeof result !== "object" || result === null)
|
|
151
|
+
return undefined;
|
|
152
|
+
const r = result;
|
|
153
|
+
if (typeof r.ok !== "boolean")
|
|
154
|
+
return undefined;
|
|
155
|
+
if (typeof r.outputHash !== "string" || !HEX_SHA256.test(r.outputHash))
|
|
156
|
+
return undefined;
|
|
157
|
+
const base = {
|
|
158
|
+
ok: r.ok,
|
|
159
|
+
outputHash: r.outputHash.toLowerCase(),
|
|
160
|
+
...(typeof r.excerpt === "string" ? { excerpt: r.excerpt } : {}),
|
|
161
|
+
};
|
|
162
|
+
if (r.callId === undefined)
|
|
163
|
+
return { ...base, runId: `verify_${randomUUID()}`, callId: `verify_${randomUUID()}` };
|
|
164
|
+
if (typeof r.callId !== "string" || !r.callId)
|
|
165
|
+
return undefined;
|
|
166
|
+
const row = db
|
|
167
|
+
.prepare("SELECT run_id FROM tool_calls WHERE session=? AND call_id=?")
|
|
168
|
+
.get(sessionId, r.callId);
|
|
169
|
+
const runId = row ? String(row.run_id ?? "") : "";
|
|
170
|
+
return runId ? { ...base, callId: r.callId, runId } : undefined;
|
|
171
|
+
}
|
package/dist/runtime/store.js
CHANGED
|
@@ -312,6 +312,66 @@ export class SQLiteStore {
|
|
|
312
312
|
this.db.exec("PRAGMA foreign_keys=ON");
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
|
+
if (!has(11))
|
|
316
|
+
this.db.transaction(() => {
|
|
317
|
+
// v11: the Proof ledger (specs/011). Criteria and evidence die with their session;
|
|
318
|
+
// evidence is append-only (the trigger rejects UPDATE). The status is never stored.
|
|
319
|
+
this.db.exec(`
|
|
320
|
+
CREATE TABLE IF NOT EXISTS proof_criteria(
|
|
321
|
+
criterion_id TEXT PRIMARY KEY,
|
|
322
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
323
|
+
goal_id TEXT,
|
|
324
|
+
text TEXT NOT NULL,
|
|
325
|
+
required INTEGER NOT NULL DEFAULT 0,
|
|
326
|
+
origin TEXT NOT NULL,
|
|
327
|
+
verifier_json TEXT,
|
|
328
|
+
scope_json TEXT,
|
|
329
|
+
na_reason TEXT,
|
|
330
|
+
created_at INTEGER NOT NULL);
|
|
331
|
+
CREATE INDEX IF NOT EXISTS proof_criteria_session ON proof_criteria(session_id, created_at);
|
|
332
|
+
CREATE TABLE IF NOT EXISTS proof_evidence(
|
|
333
|
+
evidence_id TEXT PRIMARY KEY,
|
|
334
|
+
criterion_id TEXT NOT NULL REFERENCES proof_criteria(criterion_id) ON DELETE CASCADE,
|
|
335
|
+
run_id TEXT NOT NULL,
|
|
336
|
+
call_id TEXT NOT NULL,
|
|
337
|
+
ok INTEGER NOT NULL,
|
|
338
|
+
verifier_kind TEXT NOT NULL,
|
|
339
|
+
fingerprint TEXT,
|
|
340
|
+
exit_code INTEGER,
|
|
341
|
+
duration_ms INTEGER NOT NULL,
|
|
342
|
+
output_hash TEXT NOT NULL,
|
|
343
|
+
excerpt TEXT,
|
|
344
|
+
tree_fingerprint TEXT NOT NULL,
|
|
345
|
+
files_json TEXT NOT NULL,
|
|
346
|
+
captured_at TEXT NOT NULL);
|
|
347
|
+
CREATE INDEX IF NOT EXISTS proof_evidence_criterion ON proof_evidence(criterion_id);
|
|
348
|
+
CREATE TRIGGER IF NOT EXISTS proof_evidence_append_only
|
|
349
|
+
BEFORE UPDATE ON proof_evidence
|
|
350
|
+
BEGIN SELECT RAISE(ABORT, 'proof_evidence is append-only'); END;
|
|
351
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(11);`);
|
|
352
|
+
});
|
|
353
|
+
if (!has(12))
|
|
354
|
+
this.db.transaction(() => {
|
|
355
|
+
// v12: the newest successful write_file/edit_file per path for sessions with Proof
|
|
356
|
+
// criteria (specs/011 3.4, D5). One row per path keeps it bounded; it dies with the session.
|
|
357
|
+
this.db.exec(`
|
|
358
|
+
CREATE TABLE IF NOT EXISTS proof_edits(
|
|
359
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
360
|
+
path TEXT NOT NULL,
|
|
361
|
+
edited_at TEXT NOT NULL,
|
|
362
|
+
PRIMARY KEY(session_id, path));
|
|
363
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(12);`);
|
|
364
|
+
});
|
|
365
|
+
if (!has(13))
|
|
366
|
+
this.db.transaction(() => {
|
|
367
|
+
// v13 (additive): the Proof snapshot a completed goal keeps (specs/011 3.6, O6): which
|
|
368
|
+
// required criteria were unmet and the user's override reason, as JSON. Old binaries
|
|
369
|
+
// ignore the column; goals without criteria keep it NULL.
|
|
370
|
+
const columns = new Set(this.db.prepare("PRAGMA table_info(session_goals)").all().map((c) => c.name));
|
|
371
|
+
if (!columns.has("proof_json"))
|
|
372
|
+
this.db.exec("ALTER TABLE session_goals ADD COLUMN proof_json TEXT");
|
|
373
|
+
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(13);");
|
|
374
|
+
});
|
|
315
375
|
}
|
|
316
376
|
/**
|
|
317
377
|
* The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
|
package/dist/tools/goal.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ import type { ToolRegistry } from "../core/registry.ts";
|
|
|
12
12
|
import type { GoalService } from "../goal/service.ts";
|
|
13
13
|
export declare const GET_GOAL_TOOL = "get_goal";
|
|
14
14
|
export declare const UPDATE_GOAL_TOOL = "update_goal";
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Parses the evidence list defensively (the schema is advisory for some providers). An optional
|
|
17
|
+
* `evidenceId` is kept as declared here; the goal service later drops every id that does not
|
|
18
|
+
* resolve to captured evidence of the session (the declaration is then plain text).
|
|
19
|
+
*/
|
|
16
20
|
export declare function parseEvidence(raw: unknown): GoalEvidence[] | string;
|
|
17
21
|
export declare function registerGoalTools(registry: ToolRegistry, deps: {
|
|
18
22
|
goals: GoalService;
|
package/dist/tools/goal.js
CHANGED
|
@@ -35,7 +35,13 @@ function brief(goal) {
|
|
|
35
35
|
max_minutes: Math.round(goal.maxWallMs / 60_000),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
const MAX_EVIDENCE_ID = 100;
|
|
39
|
+
const MAX_UNMET_LISTED = 20;
|
|
40
|
+
/**
|
|
41
|
+
* Parses the evidence list defensively (the schema is advisory for some providers). An optional
|
|
42
|
+
* `evidenceId` is kept as declared here; the goal service later drops every id that does not
|
|
43
|
+
* resolve to captured evidence of the session (the declaration is then plain text).
|
|
44
|
+
*/
|
|
39
45
|
export function parseEvidence(raw) {
|
|
40
46
|
if (!Array.isArray(raw) || !raw.length)
|
|
41
47
|
return "evidence must list at least one item.";
|
|
@@ -48,7 +54,12 @@ export function parseEvidence(raw) {
|
|
|
48
54
|
if (!detail)
|
|
49
55
|
return "every evidence item needs a detail (a path, a test name, a log line).";
|
|
50
56
|
const kind = KINDS.find((candidate) => candidate === item.kind) ?? "other";
|
|
51
|
-
|
|
57
|
+
const evidenceId = typeof item?.evidenceId === "string" ? item.evidenceId.trim() : "";
|
|
58
|
+
items.push({
|
|
59
|
+
kind,
|
|
60
|
+
detail: detail.slice(0, MAX_DETAIL),
|
|
61
|
+
...(evidenceId && evidenceId.length <= MAX_EVIDENCE_ID ? { evidenceId } : {}),
|
|
62
|
+
});
|
|
52
63
|
}
|
|
53
64
|
return items;
|
|
54
65
|
}
|
|
@@ -85,7 +96,15 @@ export function registerGoalTools(registry, deps) {
|
|
|
85
96
|
type: "array",
|
|
86
97
|
minItems: 1,
|
|
87
98
|
maxItems: MAX_EVIDENCE,
|
|
88
|
-
items: objectSchema({
|
|
99
|
+
items: objectSchema({
|
|
100
|
+
kind: { enum: [...KINDS] },
|
|
101
|
+
detail: { type: "string", maxLength: MAX_DETAIL },
|
|
102
|
+
evidenceId: {
|
|
103
|
+
type: "string",
|
|
104
|
+
maxLength: MAX_EVIDENCE_ID,
|
|
105
|
+
description: "Optional: the id of captured Proof evidence this item refers to. Without a valid id the item is plain text and verifies nothing.",
|
|
106
|
+
},
|
|
107
|
+
}, ["kind", "detail"]),
|
|
89
108
|
},
|
|
90
109
|
}, ["status", "summary", "evidence"]),
|
|
91
110
|
async execute(input, context) {
|
|
@@ -101,7 +120,19 @@ export function registerGoalTools(registry, deps) {
|
|
|
101
120
|
return textResult(JSON.stringify({ ok: false, error: evidence }));
|
|
102
121
|
const result = goals.report(sessionId, context.runId, { status, summary, evidence });
|
|
103
122
|
if (!result.ok)
|
|
104
|
-
return textResult(JSON.stringify({
|
|
123
|
+
return textResult(JSON.stringify({
|
|
124
|
+
ok: false,
|
|
125
|
+
error: result.message,
|
|
126
|
+
...(result.unmet
|
|
127
|
+
? {
|
|
128
|
+
unmet: result.unmet.slice(0, MAX_UNMET_LISTED).map((u) => ({
|
|
129
|
+
criterionId: u.criterionId,
|
|
130
|
+
text: u.text,
|
|
131
|
+
status: u.status,
|
|
132
|
+
})),
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
}));
|
|
105
136
|
if (status === "complete")
|
|
106
137
|
return textResult(JSON.stringify({
|
|
107
138
|
ok: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"node": ">=22.16"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/sdk": "0.
|
|
48
|
+
"@alisio/sdk": "0.8.0",
|
|
49
49
|
"@mixmark-io/domino": "2.2.0",
|
|
50
50
|
"@modelcontextprotocol/client": "2.1.0",
|
|
51
51
|
"ajv": "8.20.0",
|