@anchrd/intel-api 0.13.0 → 0.14.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/adapters/cloudflare/cloudflare.js +1 -0
- package/dist/adapters/cloudflare-api/cloudflare-api.js +136 -44
- package/dist/adapters/db/db-indexing.js +79 -0
- package/dist/adapters/db/db.js +66 -27
- package/dist/adapters/semantic-index/semantic-index.js +97 -17
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +20 -1
- package/dist/bundle/bundle.js +23 -2
- package/dist/http/http.js +4 -0
- package/dist/indexing/indexing.js +165 -16
- package/dist/indexing/indexing.types.d.ts +1 -0
- package/dist/mcp/mcp.js +45 -31
- package/dist/nodes/board/board.d.ts +3 -1
- package/dist/nodes/board/board.js +307 -9
- package/dist/nodes/board/board.types.d.ts +7 -0
- package/dist/nodes/nodes.js +146 -14
- package/dist/nodes/nodes.types.d.ts +61 -3
- package/migrations/0009_no_context_policy.sql +15 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +90 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddBoardTaskInput, AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, BoardTaskResult, ConfigureBoardInput, ConfigureBoardResult, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteBoardTaskResult, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, GetBoardInput, ListAgentsInput, ListNodesInput, MoveBoardTaskInput, Node, NodeAgent, NodeAttachment, NodeBoard, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateBoardTaskInput, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
|
|
1
|
+
import type { AddBoardTaskInput, AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, BoardTaskResult, ConfigureBoardInput, ConfigureBoardResult, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteBoardTaskResult, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, GetBoardInput, ListAgentsInput, ListNodesInput, MoveBoardTaskInput, Node, NodeAgent, NodeAttachment, NodeBoard, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, RepairBoardTaskIdsInput, RepairBoardTaskIdsResult, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateBoardTaskInput, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
|
|
2
2
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
3
|
export interface Actor {
|
|
4
4
|
id: string;
|
|
@@ -27,7 +27,7 @@ export interface NewTableVersion {
|
|
|
27
27
|
idempotencyKey: string;
|
|
28
28
|
auditId: string;
|
|
29
29
|
}
|
|
30
|
-
export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine" | "node.board_configure" | "node.board_task_add" | "node.board_task_update" | "node.board_task_move" | "node.board_task_delete";
|
|
30
|
+
export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine" | "node.board_configure" | "node.board_task_add" | "node.board_task_update" | "node.board_task_move" | "node.board_task_delete" | "node.board_task_repair";
|
|
31
31
|
/**
|
|
32
32
|
* One version that replaces the readable state, written only against the state it replaces.
|
|
33
33
|
*
|
|
@@ -145,7 +145,21 @@ export interface NodeRepository {
|
|
|
145
145
|
occurredAt: string;
|
|
146
146
|
}): Promise<boolean>;
|
|
147
147
|
searchVisible(actor: Actor, input: SearchInput): Promise<NodeCitation[]>;
|
|
148
|
-
|
|
148
|
+
/**
|
|
149
|
+
* `scopeId` cuts the candidates to one folder subtree, which is how the semantic half of a scoped
|
|
150
|
+
* search is narrowed (#126): the vector index answers over everything and this join is where the
|
|
151
|
+
* subtree — a relation in D1, not a value on a vector — is applied. The ACL still applies too.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ One entry per node, and it names WHICH chunk answered (anchrd/intel#301). The passage a
|
|
154
|
+
* searcher is shown is then that card rather than whichever one happens to be first in the board;
|
|
155
|
+
* a node whose winning chunk has no `node_vectors` row — every vector written before #301 — falls
|
|
156
|
+
* back on the first full-text passage, which is exactly what this returned before.
|
|
157
|
+
*/
|
|
158
|
+
hydrateVisibleCitations(actor: Actor, hits: Array<{
|
|
159
|
+
nodeId: string;
|
|
160
|
+
chunkKey: string;
|
|
161
|
+
}>, scopeId?: string): Promise<NodeCitation[]>;
|
|
162
|
+
invalidateVectors(): Promise<void>;
|
|
149
163
|
listCurrentVersionIds(input: {
|
|
150
164
|
after: string | null;
|
|
151
165
|
limit: number;
|
|
@@ -354,6 +368,15 @@ export interface NodeService {
|
|
|
354
368
|
updateBoardTask(actor: Actor, input: UpdateBoardTaskInput): Promise<BoardTaskResult>;
|
|
355
369
|
moveBoardTask(actor: Actor, input: MoveBoardTaskInput): Promise<BoardTaskResult>;
|
|
356
370
|
deleteBoardTask(actor: Actor, input: DeleteBoardTaskInput): Promise<DeleteBoardTaskResult>;
|
|
371
|
+
/**
|
|
372
|
+
* The sixth, and the only one that is not about somebody's project (anchrd/intel#341).
|
|
373
|
+
*
|
|
374
|
+
* ⚠️ It names no task id, because a board that needs it holds two entries under one — the pair a
|
|
375
|
+
* bundle written before anchrd/intel#321 could carry in. Everything else here is unreachable for
|
|
376
|
+
* that second entry: the five operations above address a task by id and would take, write or
|
|
377
|
+
* remove both at once.
|
|
378
|
+
*/
|
|
379
|
+
repairBoardTaskIds(actor: Actor, input: RepairBoardTaskIdsInput): Promise<RepairBoardTaskIdsResult>;
|
|
357
380
|
getAgent(actor: Actor, input: GetAgentInput): Promise<NodeAgent>;
|
|
358
381
|
/**
|
|
359
382
|
* An agent node, its first definition, and the Gate Application it runs as (#182, D29).
|
|
@@ -435,12 +458,47 @@ export interface NodeIndexTarget {
|
|
|
435
458
|
* task's own title, which is the text the FTS table weights highest.
|
|
436
459
|
*/
|
|
437
460
|
export interface IndexChunk {
|
|
461
|
+
/**
|
|
462
|
+
* What this passage is called inside its node (anchrd/intel#301).
|
|
463
|
+
*
|
|
464
|
+
* `""` for a node that is one passage — every kind but `board` — and the task's own id for a card.
|
|
465
|
+
* The full-text index does not store it; the vector index is named by it, which is how a semantic
|
|
466
|
+
* hit can say WHICH card answered instead of only which board.
|
|
467
|
+
*/
|
|
468
|
+
key: string;
|
|
438
469
|
title: string;
|
|
439
470
|
text: string;
|
|
440
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* What the vector index holds for one chunk of one node (anchrd/intel#301).
|
|
474
|
+
*
|
|
475
|
+
* `fingerprint` is over the exact text that was embedded, so the next pass can tell an untouched
|
|
476
|
+
* card from a changed one without asking Vectorize — whose writes are asynchronous and would answer
|
|
477
|
+
* about the save before last. `passage` is what a searcher is shown when this vector is the hit.
|
|
478
|
+
*/
|
|
479
|
+
export interface NodeVectorRecord {
|
|
480
|
+
chunkKey: string;
|
|
481
|
+
fingerprint: string;
|
|
482
|
+
passage: string;
|
|
483
|
+
}
|
|
441
484
|
export interface NodeIndexRepository {
|
|
442
485
|
getTarget(versionId: string): Promise<NodeIndexTarget | null>;
|
|
486
|
+
/**
|
|
487
|
+
* The node behind this version when the reason `getTarget` refused it is that the node has been
|
|
488
|
+
* ARCHIVED — and null for every other reason (anchrd/intel#348).
|
|
489
|
+
*
|
|
490
|
+
* ⚠️ The distinction is the whole method. `getTarget` answers null for two very different
|
|
491
|
+
* situations: the node is archived, or this version has been superseded by a later save. The
|
|
492
|
+
* first one means "take this node out of the vector index"; the second is a queue message that
|
|
493
|
+
* arrived late — the ordinary case, because Cloudflare Queues deliver at least once — and acting
|
|
494
|
+
* on it would delete the vectors of a node that is perfectly alive. So this asks for the archived
|
|
495
|
+
* case by name and never infers it from an absence.
|
|
496
|
+
*/
|
|
497
|
+
archivedNodeId(versionId: string): Promise<string | null>;
|
|
443
498
|
replace(target: NodeIndexTarget, chunks: IndexChunk[]): Promise<void>;
|
|
499
|
+
listVectors(nodeId: string): Promise<NodeVectorRecord[]>;
|
|
500
|
+
replaceVectors(target: NodeIndexTarget, records: NodeVectorRecord[]): Promise<void>;
|
|
501
|
+
deleteVectors(nodeId: string): Promise<void>;
|
|
444
502
|
markIndexed(versionId: string, occurredAt: string): Promise<void>;
|
|
445
503
|
markError(versionId: string, message: string, occurredAt: string): Promise<void>;
|
|
446
504
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
-- #76. `context_policy` leaves the contract, the UI and every MCP answer. The COLUMN stays.
|
|
2
2
|
--
|
|
3
|
+
-- ⚠️ HISTORY, and no longer an explanation of what is possible. The column was finally dropped by
|
|
4
|
+
-- `0018_no_context_policy_at_last.sql` (#86), through an ordinary migration file — the very thing
|
|
5
|
+
-- the conclusion at the bottom of this file says cannot be done. What changed is not D1 but the
|
|
6
|
+
-- recipe: `0005`, `0013` and `0016` worked out that the new table has to be created under the FINAL
|
|
7
|
+
-- name instead of renamed into place, and that `node_links` has to be carried out of the way
|
|
8
|
+
-- because it is the one child declared ON DELETE CASCADE. Read on for the three failures that
|
|
9
|
+
-- produced the lessons; read `0018` for the shape that works.
|
|
10
|
+
--
|
|
3
11
|
-- ⚠️ That is not the intent, it is what D1 permits. SQLite cannot drop a column a CHECK names, and
|
|
4
12
|
-- this one names itself. The way around it is a table rebuild, and `knowledge_nodes` carries six
|
|
5
13
|
-- foreign keys, one of them from itself.
|
|
@@ -22,6 +30,13 @@
|
|
|
22
30
|
-- A table rebuild with foreign keys is therefore not possible inside a migration file. It needs a
|
|
23
31
|
-- session that drives the transaction itself.
|
|
24
32
|
--
|
|
33
|
+
-- ⚠️ THAT CONCLUSION WAS WRONG, and the way it was wrong is worth more than the conclusion. The
|
|
34
|
+
-- three lessons above are all correct; what they did not contain was the fourth — do not RENAME at
|
|
35
|
+
-- all. Create the new table under the final name and insert the rows back under the name the
|
|
36
|
+
-- children have referenced the whole time, and there is nothing left for a deferred check to
|
|
37
|
+
-- complain about at COMMIT. `0005` found it, `0013` and `0016` repeated it, and `0018` used it to
|
|
38
|
+
-- drop this column at last (#86).
|
|
39
|
+
--
|
|
25
40
|
-- What holds instead: the column sits in D1, nothing reads it, and `db.ts` writes a fixed value on
|
|
26
41
|
-- insert because it is NOT NULL without a DEFAULT. The contract does not know it — for every
|
|
27
42
|
-- consumer it is gone. What remains is one dead column, and that is the price of Intel running.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- anchrd/intel#301: one vector per board card, and the record of which vectors a node has.
|
|
2
|
+
--
|
|
3
|
+
-- Vectorize is keyed by node id (`adapters/semantic-index`), so a board was ONE vector holding the
|
|
4
|
+
-- whole document while the full-text half had held one row per card since #285. "Where do I stand
|
|
5
|
+
-- with X" is the question a board exists to answer and it is an imprecise one: lexically it reaches
|
|
6
|
+
-- a card only where the searched word is written on it verbatim. A vector id therefore becomes
|
|
7
|
+
-- `<node id>#<task id>` for a board card and stays the bare node id for every other kind — no
|
|
8
|
+
-- existing vector changes its name, so no installation re-embeds its whole tree to get this.
|
|
9
|
+
--
|
|
10
|
+
-- This table is the D1 side of that index and nothing more: which vectors a node has, what text
|
|
11
|
+
-- each one was made from, and the passage a searcher is shown when that vector is the hit. It is
|
|
12
|
+
-- DERIVED like `node_fts` beside it, and `reindex` empties it so that a rebuild really rebuilds.
|
|
13
|
+
--
|
|
14
|
+
-- ⚠️ `fingerprint` is what keeps a board of three hundred cards from costing three hundred
|
|
15
|
+
-- embeddings per save. The indexing pass compares it against the text it is about to embed and
|
|
16
|
+
-- upserts only what changed; a row is written ONLY AFTER the upsert it describes succeeded. That is
|
|
17
|
+
-- why a fingerprint is stored rather than a timestamp: a pass that dies between the two leaves a
|
|
18
|
+
-- row missing, never a row claiming a vector that was never written, so the next pass repairs
|
|
19
|
+
-- itself instead of trusting a lie.
|
|
20
|
+
--
|
|
21
|
+
-- ⚠️ `chunk_key` is `''` for a node that has exactly one vector — every kind but `board`. NULL
|
|
22
|
+
-- would be the honest spelling and is the wrong one: NULLs are distinct from one another inside a
|
|
23
|
+
-- SQLite primary key, so two rows for the same node could both exist and neither would be found by
|
|
24
|
+
-- the other's write.
|
|
25
|
+
--
|
|
26
|
+
-- ⚠️ No foreign key on `node_id`, deliberately, and for the same reason `node_fts` has none: a
|
|
27
|
+
-- derived index must not be able to make a write to `nodes` fail, and every rebuild of `nodes`
|
|
28
|
+
-- (0005, 0013, 0016, 0018) has to carry each declared child through the detour those files describe.
|
|
29
|
+
-- A row that outlives its node is invisible anyway — hydration joins `nodes` and drops what is
|
|
30
|
+
-- archived or gone, exactly as it does for `node_fts`.
|
|
31
|
+
CREATE TABLE node_vectors (
|
|
32
|
+
node_id TEXT NOT NULL,
|
|
33
|
+
chunk_key TEXT NOT NULL,
|
|
34
|
+
version_id TEXT NOT NULL,
|
|
35
|
+
fingerprint TEXT NOT NULL,
|
|
36
|
+
passage TEXT NOT NULL,
|
|
37
|
+
PRIMARY KEY (node_id, chunk_key)
|
|
38
|
+
);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
-- #86: `context_policy` leaves `nodes` for good.
|
|
2
|
+
--
|
|
3
|
+
-- #76 removed it from the contract, the UI and every MCP answer — for every consumer it has been
|
|
4
|
+
-- gone since. The COLUMN stayed because SQLite cannot drop one a CHECK names, and `db.ts` has been
|
|
5
|
+
-- writing the fixed value `'relevant'` into it ever since so that inserting a node works at all.
|
|
6
|
+
-- A dead column plus a line of code serving it: harmless, and ballast the next reader has to be
|
|
7
|
+
-- told about.
|
|
8
|
+
--
|
|
9
|
+
-- ⚠️ THE reason this ticket sat open for weeks is that three earlier attempts failed against
|
|
10
|
+
-- `--remote` while passing locally, and the file that recorded them (`0009_no_context_policy.sql`)
|
|
11
|
+
-- concluded that a table rebuild with foreign keys needs "a session that runs the transaction
|
|
12
|
+
-- itself, not a migration file". That conclusion is out of date, and the proof is in this
|
|
13
|
+
-- directory: `0005`, `0013` and `0016` each rebuilt this very table through an ordinary migration
|
|
14
|
+
-- file. `0016` did it against the live database on 2026-08-08 with 120 nodes, and every referencing
|
|
15
|
+
-- table came out with the same count it went in with.
|
|
16
|
+
--
|
|
17
|
+
-- ⚠️ One thing about that run is worth knowing before it is quoted as a precedent: the installation
|
|
18
|
+
-- held ZERO `node_links` rows, so the rescue below would have had nothing to rescue and the run
|
|
19
|
+
-- would have passed without exercising it at all. The one row it did carry was created for the
|
|
20
|
+
-- purpose, minutes before, by linking two throwaway documents. The proof was arranged, not found —
|
|
21
|
+
-- and the next person rebuilding this table has to arrange it again, because the installation still
|
|
22
|
+
-- has almost no links.
|
|
23
|
+
--
|
|
24
|
+
-- What the three of them worked out, and what this file copies rather than rediscovers:
|
|
25
|
+
--
|
|
26
|
+
-- 1. `PRAGMA foreign_keys = OFF` is IGNORED by D1 over the HTTP API. Locally miniflare obeys it,
|
|
27
|
+
-- so the integration test was green while the real database answered `FOREIGN KEY constraint
|
|
28
|
+
-- failed`. That is why the proof of a migration here is a run against `--remote`.
|
|
29
|
+
-- 2. The new table is created under the FINAL name rather than built beside the old one and
|
|
30
|
+
-- renamed over it. `ALTER TABLE ... RENAME` makes references FOLLOW the rename, so renaming
|
|
31
|
+
-- the old table out of the way quietly re-points every other table at a table about to be
|
|
32
|
+
-- dropped. Inserting the rows again under the name the children have referenced all along is
|
|
33
|
+
-- what settles them.
|
|
34
|
+
-- 3. `DROP TABLE` on a parent runs an implicit `DELETE FROM` first, and `node_links` is the one
|
|
35
|
+
-- child declared ON DELETE CASCADE — so that delete does not merely flag its rows, it REMOVES
|
|
36
|
+
-- them. They are carried out of the way and put back. That is a rescue, not a decision about
|
|
37
|
+
-- the data.
|
|
38
|
+
--
|
|
39
|
+
-- ⚠️ On an empty database neither detour is visible, because nothing points at anything. That is
|
|
40
|
+
-- how `0005`'s first version passed a green suite and then failed against the first database with
|
|
41
|
+
-- content in it, and why the proof for this file is a row count of every referencing table before
|
|
42
|
+
-- and after rather than a migration that merely ran.
|
|
43
|
+
PRAGMA defer_foreign_keys = TRUE;
|
|
44
|
+
|
|
45
|
+
-- Plain holding tables on purpose: no keys, no CHECKs, no foreign keys, and the column set taken
|
|
46
|
+
-- from whatever the live table has. Anything enforced here would only be enforced a second time on
|
|
47
|
+
-- the way back in, and a holding table that can reject a row is a holding table that can lose one.
|
|
48
|
+
CREATE TABLE nodes_carry AS SELECT * FROM nodes;
|
|
49
|
+
CREATE TABLE node_links_carry AS SELECT * FROM node_links;
|
|
50
|
+
|
|
51
|
+
DROP TABLE nodes;
|
|
52
|
+
|
|
53
|
+
-- The same table as `0016` left it, minus one column. Nothing else about it changes: same keys,
|
|
54
|
+
-- same CHECKs, same six kinds — a rebuild is the only way to drop the column, not an invitation to
|
|
55
|
+
-- change anything else while the table is open.
|
|
56
|
+
CREATE TABLE nodes (
|
|
57
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
58
|
+
parent_id TEXT REFERENCES nodes(id),
|
|
59
|
+
kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table', 'agent', 'board')),
|
|
60
|
+
title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 240),
|
|
61
|
+
description TEXT CHECK (description IS NULL OR length(description) <= 2000),
|
|
62
|
+
owner_id TEXT NOT NULL,
|
|
63
|
+
current_version_id TEXT,
|
|
64
|
+
created_at TEXT NOT NULL,
|
|
65
|
+
updated_at TEXT NOT NULL,
|
|
66
|
+
archived_at TEXT
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
-- Columns named on both sides rather than `SELECT *`: the holding table still HAS `context_policy`,
|
|
70
|
+
-- and a positional insert would either fail or, worse, shift every value one place to the left.
|
|
71
|
+
INSERT INTO nodes (
|
|
72
|
+
id, parent_id, kind, title, description, owner_id,
|
|
73
|
+
current_version_id, created_at, updated_at, archived_at
|
|
74
|
+
)
|
|
75
|
+
SELECT
|
|
76
|
+
id, parent_id, kind, title, description, owner_id,
|
|
77
|
+
current_version_id, created_at, updated_at, archived_at
|
|
78
|
+
FROM nodes_carry;
|
|
79
|
+
|
|
80
|
+
-- `OR IGNORE` because whether the cascade above actually fired is SQLite's business, not this
|
|
81
|
+
-- migration's: if it did, this puts the rows back; if it did not, each one is already present under
|
|
82
|
+
-- the same primary key and this is a no-op. Either way `node_links` ends up holding exactly what it
|
|
83
|
+
-- held before, which is the only outcome this statement is permitted to have.
|
|
84
|
+
INSERT OR IGNORE INTO node_links SELECT * FROM node_links_carry;
|
|
85
|
+
|
|
86
|
+
DROP TABLE nodes_carry;
|
|
87
|
+
DROP TABLE node_links_carry;
|
|
88
|
+
|
|
89
|
+
CREATE INDEX nodes_parent_idx ON nodes(parent_id, archived_at, title);
|
|
90
|
+
CREATE INDEX nodes_owner_idx ON nodes(owner_id, archived_at);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@anchrd/gate-sdk": "^0.7.0",
|
|
46
|
-
"@anchrd/intel-contract": "^0.
|
|
46
|
+
"@anchrd/intel-contract": "^0.12.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
49
|
"fflate": "^0.8.3",
|