@anchrd/intel-api 0.37.1 → 0.39.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/db/db.js +80 -8
- package/dist/mcp/mcp.js +79 -8
- package/dist/nodes/nodes.js +35 -2
- package/dist/nodes/nodes.types.d.ts +11 -0
- package/package.json +3 -3
package/dist/adapters/db/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { descendantsCte, grantColumns as grantColumnsFor, grantInForce, mapGrant, nodeVerbBindings, nodeVerbQuery, principalColumns, subtreeBindings, subtreeCte, } from "./db-grants.js";
|
|
1
|
+
import { descendantsCte, flowInSubtree, flowInSubtreeBindings, grantColumns as grantColumnsFor, grantInForce, mapGrant, nodeVerbBindings, nodeVerbQuery, principalColumns, subtreeBindings, subtreeCte, } from "./db-grants.js";
|
|
2
2
|
const versionColumns = `id, node_id, sequence, content_key, media_type, content_hash,
|
|
3
3
|
size, segment, created_by, created_at`;
|
|
4
4
|
const grantColumns = grantColumnsFor("node_id");
|
|
@@ -70,6 +70,18 @@ export function createNodeRepository(deps) {
|
|
|
70
70
|
// same question of the same table.
|
|
71
71
|
const visibleCte = subtreeCte;
|
|
72
72
|
const readBindings = (actor) => subtreeBindings(actor, "read", deps.now().toISOString());
|
|
73
|
+
/**
|
|
74
|
+
* What a LEVEL query binds: the walk over `nodes`, then the flow half of `has_children`, in the
|
|
75
|
+
* order the two appear in the statement.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ One clock reading for both halves. Two calls to `deps.now()` inside one statement would
|
|
78
|
+
* measure the same `expires_at` against two different moments, and a grant that expires between
|
|
79
|
+
* them would be honoured on one side of the OR and refused on the other.
|
|
80
|
+
*/
|
|
81
|
+
const levelBindings = (actor) => {
|
|
82
|
+
const now = deps.now().toISOString();
|
|
83
|
+
return [...subtreeBindings(actor, "read", now), ...flowInSubtreeBindings(actor, "read", now)];
|
|
84
|
+
};
|
|
73
85
|
/**
|
|
74
86
|
* The optional folder cut a search carries (#126), in the three places one statement needs it:
|
|
75
87
|
* the extra CTE, the extra join, and the one binding between the actor's and the query's. All
|
|
@@ -126,8 +138,14 @@ export function createNodeRepository(deps) {
|
|
|
126
138
|
* everywhere would make a link to a task a dead end.
|
|
127
139
|
*/
|
|
128
140
|
const notInTree = (alias) => `${alias}.kind <> 'task'`;
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The chevron column, and it is drawn ONLY for the tree (#777). The bounded read the relation
|
|
143
|
+
* graph makes discards it, and since #777 it is no longer free: computing it costs a second
|
|
144
|
+
* correlated `EXISTS` per row and six bindings that statement has no other use for. Leaving it
|
|
145
|
+
* out there is not a divergence in the visibility predicate, which is what the two forms share;
|
|
146
|
+
* it is one column the caller never reads.
|
|
147
|
+
*/
|
|
148
|
+
const chevronColumn = `
|
|
131
149
|
-- Whether this row has children THIS actor may see (#59), asked of the same allowed set one
|
|
132
150
|
-- level down. A second rule written here would drift from the one above it, and the drift
|
|
133
151
|
-- would show as a chevron that opens onto nothing.
|
|
@@ -137,11 +155,29 @@ export function createNodeRepository(deps) {
|
|
|
137
155
|
-- actor can see, and the chevron falls away by itself (D66, #376). Leaving it out here would
|
|
138
156
|
-- draw a chevron that opens onto an empty level — exactly the drift the comment above warns
|
|
139
157
|
-- about, arriving through the door it was written for.
|
|
140
|
-
|
|
158
|
+
--
|
|
159
|
+
-- ⚠️ And the SECOND half of the level, because the tree is shared: a folder holds nodes and
|
|
160
|
+
-- flows side by side (ADR-0004 §1), and a flow is not a node. Asking only the nodes table
|
|
161
|
+
-- left a folder whose only children are flows answering "empty": no chevron, no way down,
|
|
162
|
+
-- while clicking the folder listed both kinds perfectly well (#777). It is the same root as
|
|
163
|
+
-- #457, where a count over nodes alone held such a folder for empty on the delete path; that
|
|
164
|
+
-- one was fixed, this one was not.
|
|
165
|
+
--
|
|
166
|
+
-- ⚠️ flowInSubtree rather than a hand-written condition, for the reason stated above it: it
|
|
167
|
+
-- is the very predicate db-flows.ts draws the level with, so the chevron and the level it
|
|
168
|
+
-- opens onto cannot answer differently. Its folder clause is already satisfied here (the
|
|
169
|
+
-- flow's parent is n, and n is joined to allowed), so today it can only widen the answer,
|
|
170
|
+
-- never narrow it, which is the only direction a chevron may drift in at all.
|
|
171
|
+
(EXISTS (
|
|
141
172
|
SELECT 1 FROM nodes child
|
|
142
173
|
JOIN allowed AS allowed_child ON allowed_child.id = child.id
|
|
143
174
|
WHERE child.parent_id = n.id AND child.archived_at IS NULL AND ${notInTree("child")}
|
|
144
|
-
)
|
|
175
|
+
) OR EXISTS (
|
|
176
|
+
SELECT 1 FROM flows flow
|
|
177
|
+
WHERE flow.parent_id = n.id AND flow.archived_at IS NULL AND ${flowInSubtree}
|
|
178
|
+
)) AS has_children`;
|
|
179
|
+
const visibleChildren = (bounded) => `${visibleCte}
|
|
180
|
+
SELECT ${nodeColumns}${bounded ? ", COUNT(*) OVER () AS total" : `,${chevronColumn}`}
|
|
145
181
|
FROM nodes n
|
|
146
182
|
JOIN allowed ON allowed.id = n.id
|
|
147
183
|
WHERE ${levelPredicate} AND (? = 1 OR n.archived_at IS NULL) AND ${notInTree("n")}
|
|
@@ -166,9 +202,17 @@ export function createNodeRepository(deps) {
|
|
|
166
202
|
async listVisible(actor, input) {
|
|
167
203
|
const result = await deps.db
|
|
168
204
|
.prepare(input.archivedOnly ? archivedEverywhere : visibleChildren(false))
|
|
169
|
-
.bind(
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
.bind(
|
|
206
|
+
// ⚠️ The archive statement answers `has_children` as a literal 0, so it carries no flow
|
|
207
|
+
// bindings; the level statement carries both halves. Same branch, two bind lists.
|
|
208
|
+
...(input.archivedOnly
|
|
209
|
+
? readBindings(actor)
|
|
210
|
+
: [
|
|
211
|
+
...levelBindings(actor),
|
|
212
|
+
input.parentId,
|
|
213
|
+
input.parentId,
|
|
214
|
+
input.includeArchived ? 1 : 0,
|
|
215
|
+
]))
|
|
172
216
|
.all();
|
|
173
217
|
const rows = result.results ?? [];
|
|
174
218
|
return {
|
|
@@ -179,6 +223,9 @@ export function createNodeRepository(deps) {
|
|
|
179
223
|
async listVisibleBounded(actor, input) {
|
|
180
224
|
const result = await deps.db
|
|
181
225
|
.prepare(visibleChildren(true))
|
|
226
|
+
// ⚠️ `readBindings`, not `levelBindings`: this form carries no chevron column, so the
|
|
227
|
+
// flow half's six bindings have no placeholder to land in. D1 answers a miscount with
|
|
228
|
+
// `Wrong number of parameter bindings`, so the two cannot silently go out of step.
|
|
182
229
|
.bind(...readBindings(actor), input.parentId, input.parentId, 0, input.limit)
|
|
183
230
|
.all();
|
|
184
231
|
const rows = result.results ?? [];
|
|
@@ -1340,6 +1387,31 @@ export function createNodeRepository(deps) {
|
|
|
1340
1387
|
.all();
|
|
1341
1388
|
return { nodes, links: (linkResult.results ?? []).map(mapLink) };
|
|
1342
1389
|
},
|
|
1390
|
+
/**
|
|
1391
|
+
* The same `allowed` set every other read joins against, narrowed to attachments (#773).
|
|
1392
|
+
*
|
|
1393
|
+
* ⚠️ Ordered and paged by `n.id` rather than by title: the id is the cursor, and a cursor has to
|
|
1394
|
+
* be unique or a page boundary falling between two identical titles silently drops one of them.
|
|
1395
|
+
* `graphVisible` above orders by title because it draws a picture and takes the whole set.
|
|
1396
|
+
*/
|
|
1397
|
+
async listVisibleAttachments(actor, input) {
|
|
1398
|
+
const result = await deps.db
|
|
1399
|
+
.prepare(`${visibleCte}
|
|
1400
|
+
SELECT ${nodeColumns}
|
|
1401
|
+
FROM nodes n
|
|
1402
|
+
JOIN allowed ON allowed.id = n.id
|
|
1403
|
+
WHERE n.archived_at IS NULL
|
|
1404
|
+
AND n.kind = 'attachment'
|
|
1405
|
+
AND (? IS NULL OR n.id > ?)
|
|
1406
|
+
ORDER BY n.id
|
|
1407
|
+
LIMIT ?`)
|
|
1408
|
+
// ⚠️ Positional throughout, and the cursor is bound TWICE. Numbered parameters (?1, ?2)
|
|
1409
|
+
// would count from the first placeholder of the whole statement — and `visibleCte` above
|
|
1410
|
+
// has already spent several of them.
|
|
1411
|
+
.bind(...readBindings(actor), input.after, input.after, input.limit)
|
|
1412
|
+
.all();
|
|
1413
|
+
return (result.results ?? []).map(mapNode);
|
|
1414
|
+
},
|
|
1343
1415
|
async setGrant(input) {
|
|
1344
1416
|
const grant = input.grant;
|
|
1345
1417
|
const { type: principalType, id: principalId } = principalColumns(grant.principal);
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BoardAssigneeResolveInput, BoardAssigneeSearchInput, BoardGetInput, Boa
|
|
|
4
4
|
import { FeedListRequest } from "@anchrd/intel-contract/feed";
|
|
5
5
|
import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, PurgeFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "@anchrd/intel-contract/flow";
|
|
6
6
|
import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "@anchrd/intel-contract/flow-run";
|
|
7
|
-
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
7
|
+
import { ArchiveNodeInput, CreateNodeInput, GetAttachmentInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
8
8
|
import { ListEffectiveAccessInput, ListFlowEffectiveAccessInput, ListFlowGrantsInput, ListGrantsInput, RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "@anchrd/intel-contract/share";
|
|
9
9
|
import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, GetTableInput, RedefineTableInput, UpdateTableRowsInput, } from "@anchrd/intel-contract/table";
|
|
10
10
|
import { ExecuteToolInput, TestToolInput } from "@anchrd/intel-contract/tool";
|
|
@@ -218,7 +218,7 @@ export async function handleMcp(request, deps) {
|
|
|
218
218
|
}, async (input) => text(await deps.boards.resolveAssignees(actor, input)));
|
|
219
219
|
server.registerTool("board_task_create", {
|
|
220
220
|
title: "Add a card to a board",
|
|
221
|
-
description: "File a new card on a board. Omit the status to put it in the first column. Name a parent task to make it a subtask — it lands on the same board, and moving an existing card under another one is `node_update` with a new parent. The card is a node like any other: its own address, its own permissions, its own history.",
|
|
221
|
+
description: "File a new card on a board. Omit the status to put it in the first column. Name a parent task to make it a subtask — it lands on the same board, and moving an existing card under another one is `node_update` with a new parent. The card is a node like any other: its own address, its own permissions, its own history — and its own body. This call answers with the BOARD rather than the new card, so to write that body, make the card with `node_create` under the board instead: it answers with the node itself, files it on the board just the same, and hands you the id `node_version_create` asks for.",
|
|
222
222
|
inputSchema: BoardTaskCreateInput,
|
|
223
223
|
annotations: {
|
|
224
224
|
title: "Add a card to a board",
|
|
@@ -329,8 +329,11 @@ export async function handleMcp(request, deps) {
|
|
|
329
329
|
}, async (input) => text(await deps.nodes.getVersion(actor, input.nodeId, input.versionId)));
|
|
330
330
|
server.registerTool("node_attachment_get", {
|
|
331
331
|
title: "Get node attachment",
|
|
332
|
-
description: "Get authorized immutable attachment metadata and
|
|
333
|
-
|
|
332
|
+
description: "Get authorized immutable attachment metadata, and with includeContent the file itself. " +
|
|
333
|
+
"The resourceUri in the answer is this server's own address; a portal that aggregates " +
|
|
334
|
+
"several servers prefixes it, so read the file through includeContent unless the URI " +
|
|
335
|
+
"came from resources/list.",
|
|
336
|
+
inputSchema: GetAttachmentInput,
|
|
334
337
|
annotations: {
|
|
335
338
|
title: "Get node attachment",
|
|
336
339
|
readOnlyHint: true,
|
|
@@ -338,8 +341,76 @@ export async function handleMcp(request, deps) {
|
|
|
338
341
|
idempotentHint: true,
|
|
339
342
|
openWorldHint: false,
|
|
340
343
|
},
|
|
341
|
-
},
|
|
342
|
-
|
|
344
|
+
},
|
|
345
|
+
/**
|
|
346
|
+
* ⚠️ The size is checked BEFORE the stream is opened, which the resource handler below cannot
|
|
347
|
+
* do — it is handed the body by the SDK and has to cancel it. Here the metadata read is its
|
|
348
|
+
* own call, so an oversized attachment costs no R2 read at all.
|
|
349
|
+
*
|
|
350
|
+
* ⚠️ The bytes go out as an embedded RESOURCE block, never as text. A base64 string in a text
|
|
351
|
+
* block is something a model will try to read; a resource block is a file to a client that
|
|
352
|
+
* understands one, and the metadata stays in the first block either way (#773).
|
|
353
|
+
*/
|
|
354
|
+
async (input) => {
|
|
355
|
+
const metadata = await deps.nodes.getAttachment(actor, input.nodeId);
|
|
356
|
+
if (!input.includeContent)
|
|
357
|
+
return text(metadata);
|
|
358
|
+
if (metadata.version.size > AttachmentInlineLimit) {
|
|
359
|
+
throw new IntelError(413, "attachment_too_large", "Attachment is too large to inline; read it over HTTP instead");
|
|
360
|
+
}
|
|
361
|
+
const { attachment, body } = await deps.nodes.readAttachment(actor, input.nodeId);
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{ type: "text", text: JSON.stringify(attachment) },
|
|
365
|
+
{
|
|
366
|
+
type: "resource",
|
|
367
|
+
resource: {
|
|
368
|
+
uri: `intel://nodes/${attachment.node.id}/attachment`,
|
|
369
|
+
mimeType: attachment.version.mediaType,
|
|
370
|
+
blob: await base64(body),
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
isError: false,
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
server.registerResource("node-attachment",
|
|
378
|
+
/**
|
|
379
|
+
* ⚠️ `list` is what makes the attachments findable at all, and it was `undefined` until #773.
|
|
380
|
+
* A client that cannot enumerate them has to GUESS the URI — and the one place it could copy
|
|
381
|
+
* one from, `node_attachment_get`, answers this server's own address, which a portal that
|
|
382
|
+
* aggregates several servers prefixes. Measured on 2026-08-24: the URI out of that tool was
|
|
383
|
+
* refused as `Resource not found` while the prefixed spelling delivered the file.
|
|
384
|
+
*
|
|
385
|
+
* ⚠️ The listing is COMPLETE, and the loop below is why it has to be. `ListResourcesCallback`
|
|
386
|
+
* takes `RequestHandlerExtra` and nothing else — the SDK hands a template's list callback no
|
|
387
|
+
* cursor (checked against the 1.30.0 type: `(extra) => ListResourcesResult`), so a
|
|
388
|
+
* `nextCursor` in the answer is one nothing would ever send back. That leaves two honest
|
|
389
|
+
* options and one dishonest one: answer everything, refuse, or cut the list at some limit and
|
|
390
|
+
* let it read as complete. The cut is the one that is out — a reader would never learn which
|
|
391
|
+
* of their files are missing.
|
|
392
|
+
*
|
|
393
|
+
* The paging is therefore INTERNAL: D1 is asked in pages so one statement never has to carry
|
|
394
|
+
* the whole set, and the loop ends when the service stops handing back a cursor.
|
|
395
|
+
*/
|
|
396
|
+
new ResourceTemplate("intel://nodes/{nodeId}/attachment", {
|
|
397
|
+
list: async () => {
|
|
398
|
+
const resources = [];
|
|
399
|
+
let after = null;
|
|
400
|
+
do {
|
|
401
|
+
const page = await deps.nodes.listAttachments(actor, { limit: 100, after });
|
|
402
|
+
resources.push(...page.items.map((node) => ({
|
|
403
|
+
uri: `intel://nodes/${node.id}/attachment`,
|
|
404
|
+
name: node.title,
|
|
405
|
+
// ⚠️ No `mimeType`: it lives on the VERSION, so answering one would cost a version
|
|
406
|
+
// read per row. The read itself carries the real one.
|
|
407
|
+
description: node.description ?? undefined,
|
|
408
|
+
})));
|
|
409
|
+
after = page.nextCursor;
|
|
410
|
+
} while (after !== null);
|
|
411
|
+
return { resources };
|
|
412
|
+
},
|
|
413
|
+
}), { title: "Intel node attachment" }, async (uri, variables) => {
|
|
343
414
|
const { attachment, body } = await deps.nodes.readAttachment(actor, String(variables.nodeId));
|
|
344
415
|
if (attachment.version.size > AttachmentInlineLimit) {
|
|
345
416
|
await body.cancel();
|
|
@@ -468,7 +539,7 @@ export async function handleMcp(request, deps) {
|
|
|
468
539
|
if (permits(deps.authorization, "nodes", "create")) {
|
|
469
540
|
server.registerTool("node_create", {
|
|
470
541
|
title: "Create node",
|
|
471
|
-
description: "Create a governed folder, document,
|
|
542
|
+
description: "Create a governed node: a folder, a board, a document, a card, an attachment or a table. A card made under a board lands on that board, and the answer carries the new node's id — which is what `node_version_create` needs to give it a body.",
|
|
472
543
|
inputSchema: CreateNodeInput,
|
|
473
544
|
annotations: {
|
|
474
545
|
title: "Create node",
|
|
@@ -482,7 +553,7 @@ export async function handleMcp(request, deps) {
|
|
|
482
553
|
if (permits(deps.authorization, "nodes", "write")) {
|
|
483
554
|
server.registerTool("node_version_create", {
|
|
484
555
|
title: "Create node version",
|
|
485
|
-
description: "Append an immutable content version using an optimistic base version.",
|
|
556
|
+
description: "Append an immutable content version using an optimistic base version. Documents and board cards both take one — writing a card here is how a ticket gets a body instead of only a title.",
|
|
486
557
|
inputSchema: SaveNodeVersionInput,
|
|
487
558
|
annotations: {
|
|
488
559
|
title: "Create node version",
|
package/dist/nodes/nodes.js
CHANGED
|
@@ -27,6 +27,22 @@ const nothingWithheld = { titles: [], hidden: 0 };
|
|
|
27
27
|
function applicableVerbs(kind) {
|
|
28
28
|
return kind === "folder" ? ["read", "write", "execute", "share"] : ["read", "write", "share"];
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Which kinds hold an editor body — the text somebody types and `node_get` reads back.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ **A card is one of them, and that is the whole of D66 on the write side** (#785). A task is a
|
|
34
|
+
* node like every other: its text is a node version, in the same table, through the same route. The
|
|
35
|
+
* panel reaches this call through `POST /nodes/:id/versions` exactly as MCP does, so a list that
|
|
36
|
+
* omitted `task` closed BOTH surfaces at once — an agent could name a card and not describe it, and
|
|
37
|
+
* a reader typing into the card got "could not be saved" over a body the model says it should hold.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ **A set, not one more `||`.** The other four kinds each carry their content some other way —
|
|
40
|
+
* a folder and a board hold children, an attachment holds bytes, a table holds CSV segments — and
|
|
41
|
+
* naming the ones that DO take an editor version is what keeps the next kind from being waved
|
|
42
|
+
* through by a condition that only ever grew. The annotation ties it to the enum: a kind renamed in
|
|
43
|
+
* the contract stops this file from compiling instead of silently dropping out of the set.
|
|
44
|
+
*/
|
|
45
|
+
const EditorContentKinds = ["document", "task"];
|
|
30
46
|
// ⚠️ The refusal has to be actionable without becoming a directory of the tree. Whoever holds
|
|
31
47
|
// `share` on one folder must not learn the titles of flows they may not see, so the ones they may
|
|
32
48
|
// see are named and the rest are only counted (ADR-0004 §3, and #17's review) — which is the whole
|
|
@@ -666,8 +682,8 @@ export function createNodes(deps) {
|
|
|
666
682
|
return document;
|
|
667
683
|
}
|
|
668
684
|
const node = await requireVisible(actor, input.nodeId);
|
|
669
|
-
if (node.kind
|
|
670
|
-
throw new IntelError(409, "document_content_required", "Only documents accept editor content versions");
|
|
685
|
+
if (!EditorContentKinds.includes(node.kind)) {
|
|
686
|
+
throw new IntelError(409, "document_content_required", "Only documents and cards accept editor content versions");
|
|
671
687
|
}
|
|
672
688
|
if (!(await deps.repository.can(actor, node.id, "write"))) {
|
|
673
689
|
throw new IntelError(403, "node_forbidden", "This node cannot be edited");
|
|
@@ -782,6 +798,23 @@ export function createNodes(deps) {
|
|
|
782
798
|
throw new IntelError(500, "content_missing", "Attachment is missing");
|
|
783
799
|
return { attachment: metadata, body };
|
|
784
800
|
},
|
|
801
|
+
/**
|
|
802
|
+
* One page of readable attachments for the MCP resource listing (#773).
|
|
803
|
+
*
|
|
804
|
+
* ⚠️ It asks the repository for one row MORE than it hands back. Without that, a page that
|
|
805
|
+
* happens to be exactly full is indistinguishable from a page with more behind it, and the
|
|
806
|
+
* choice is between always sending a cursor (one wasted round trip per listing) or never
|
|
807
|
+
* sending one (a silently truncated list, which reads as complete).
|
|
808
|
+
*/
|
|
809
|
+
async listAttachments(actor, input) {
|
|
810
|
+
const rows = await deps.repository.listVisibleAttachments(actor, {
|
|
811
|
+
limit: input.limit + 1,
|
|
812
|
+
after: input.after,
|
|
813
|
+
});
|
|
814
|
+
const items = rows.slice(0, input.limit);
|
|
815
|
+
const last = items.at(-1);
|
|
816
|
+
return { items, nextCursor: rows.length > input.limit && last ? last.id : null };
|
|
817
|
+
},
|
|
785
818
|
async getTable(actor, nodeId) {
|
|
786
819
|
const node = await requireVisible(actor, nodeId);
|
|
787
820
|
if (node.kind !== "table") {
|
|
@@ -56,6 +56,10 @@ export interface NodeRepository {
|
|
|
56
56
|
}): Promise<BoundedChildren>;
|
|
57
57
|
getVisible(actor: Actor, nodeId: string): Promise<Node | null>;
|
|
58
58
|
listVisibleSubtree(actor: Actor, rootId: string | null): Promise<SubtreeNode[]>;
|
|
59
|
+
listVisibleAttachments(actor: Actor, input: {
|
|
60
|
+
limit: number;
|
|
61
|
+
after: string | null;
|
|
62
|
+
}): Promise<Node[]>;
|
|
59
63
|
can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
|
|
60
64
|
findIdempotentNode(actorId: string, operation: "node.create" | "node.save" | "node.append" | "node.update" | "node.archive" | "node.share" | "node.revoke" | SnapshotOperation, idempotencyKey: string): Promise<string | null>;
|
|
61
65
|
findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
|
|
@@ -325,6 +329,13 @@ export interface NodeService {
|
|
|
325
329
|
redefineTable(actor: Actor, input: RedefineTableInput): Promise<NodeTable>;
|
|
326
330
|
getAttachment(actor: Actor, nodeId: string): Promise<NodeAttachment>;
|
|
327
331
|
readAttachment(actor: Actor, nodeId: string): Promise<NodeAttachmentBody>;
|
|
332
|
+
listAttachments(actor: Actor, input: {
|
|
333
|
+
limit: number;
|
|
334
|
+
after: string | null;
|
|
335
|
+
}): Promise<{
|
|
336
|
+
items: Node[];
|
|
337
|
+
nextCursor: string | null;
|
|
338
|
+
}>;
|
|
328
339
|
listVersions(actor: Actor, nodeId: string): Promise<{
|
|
329
340
|
items: NodeVersion[];
|
|
330
341
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@anchrd/gate-sdk": "^0.
|
|
46
|
-
"@anchrd/intel-contract": "^0.
|
|
45
|
+
"@anchrd/gate-sdk": "^0.26.0",
|
|
46
|
+
"@anchrd/intel-contract": "^0.31.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
49
|
"fflate": "^0.8.3",
|